I ran the code in a simple command line app (using the default Xcode template, which includes a main.swift
file for the code). The top level code thus runs on the main actor and I wanted to ensure that there is an isolation context switch in there somewhere. I understood your code snippet as meaning:
"vm
is instantiated outside of any actor isolation, then we start a Task
to call onButtonPress
. That call is isolated to @MainActor
so would require a hop, but outside the Task
the call to anotherCall
happens, which is not isolated. That results in an uncaught race"
Just pasting your code into the template (on the top level) would not be enough as we start on code that runs on the main actor already. While anotherCall
is not defined to enforce isolation to a specific actor (nonisolated
), just calling it in that scope still runs it on the main actor[1]. Even though that happens after starting a Task
, which, without explicit annotation, also inherits the @MainActor
isolation. This means all calls run on the main actor, there is no race, and the compiler correctly does not complain.
Again, that is just due to the fact the sample app I used starts on the main actor.
EDIT: I am an idiot... I link SE-0388 below, but forgot it affects this specific place...
You're right, the call to anotherCall
should emit a compiler error! I'll leave below parts in here as they show that weirdly enough, wrapping everything in a detached Task does correctly warn you...
To reproduce the issue you illustrated I wrapped your code snippet into a detached Task
, that allows me to basically "escape" the main actor. Now vm
is no longer instantiated on the main actor and the call to anotherCall
no longer runs on the main actor either (just like in your snippet).
If we were to just call vm.onButtonPress()
[2] we mess up: We send vm
to a different isolation context, main actor, but it's still used afterwards and cannot be treated as sending
. Now that I look at it again, it actually doesn't matter whether we put the vm.onButtonPressed()
call into a Task
(another one inside my detached one), the underlying problem is the same.
I think this is basically exactly what you showed in your snippet, but as said, I do see the compiler complain, at least in Swift6 mode (and btw, now that I checked again, also in Swift5 mode with anything but minimal concurrency checking).
I'm not sure I get you right here, but I think that's not the exact issue. The function is nonisolated
, yes, and due to SE-0338 (and before any changes to that) it means it hops off the current actor (thus sending vm
to a different isolation context), but that is only a problem if any other calls would still be able to use vm
after the hop happened.
Ultimately that's not different from the hop vm
needs to do for onButtonPress
that is needed when vm
wasn't instantiated on the main actor in the first place: Any subsequent references to it on the original isolation context prevent it from being treated as sending
, so the compiler complains.
I like to think of sending
as the "weaker Sendable
": You can "send" non-Sendable
objects over to another isolation context, you just have to ensure that nothing can mutate them on the original context afterwards.
In our sample code that means it is the conjunction of onButtonPressed
and anotherCall
that results in an error.
There may be more, but I don't see any uncaught race-conditions myself. That doesn't mean much as I am relatively new to sending
...
Definitely, though in our case I think the result is not as severe as you get at least some error that prevents you from compiling a data-race (again, unless I missed something...)