Imagine I'm in a class annotated with @MainActor
so that all the functions are tied to the main actor. I'm trying to understand what the difference is between doing the following:
func bar() {
Task.detached(priority: .background) {
await foo()
}
}
func foo() async {
...
}
vs
func bar() {
Task(priority: .background) {
await foo()
}
}
nonisolated func foo() async {
...
}
Are they the same?
You said:
Imagine I'm in a class annotated with
@MainActor
so that all the functions are tied to the main actor. I'm trying to understand what the difference is between doing the following:func bar() { Task.detached(priority: .background) { await foo() } } func foo() async { … }
The bar
method creates a non-structured task, which, because it is a detached
task, is not on the current actor. But that task will await
the foo
method, which will run on the main actor. The fact that foo
is in a class bearing the @MainActor
designation means that it is isolated to that actor, regardless of which Swift concurrency context you invoke it.
But, in this case, there is little point in launching a detached task that only awaits an actor-isolated function.
You continue:
vs
func bar() { Task(priority: .background) { await foo() } } nonisolated func foo() async { … }
In this case, bar
will launch a task on the current (main) actor, but it will await the result of foo
, which is nonisolated (i.e., not isolated to the main actor).
So, the difference is that the first example, foo
is actor-isolated, but it is not in the second example.
Effective Swift 5.7 (by virtue of SE-0338), Apple has formalized the behavior here, namely that “async
functions that are not actor-isolated should formally run on a generic executor associated with no actor” (i.e., if invoked from the main actor, the nonisolated async
function will not run on the main thread.
Consider:
func bar() {
Task {
await foo()
}
}
nonisolated func foo() async {
…
}
This will achieve what you want. In this case, even if bar
is isolated to the main actor (and thus its Task
is also isolated to the main actor), foo
will not run on the main actor. This pattern is a nice simple way of getting work off the main thread. And you do not need to introduce detached tasks.
Now, in the interest of full disclosure, in Swift 6, there is a further refinement to this rule (by virtue of SE-0444) where the caller can optionally specify a preferred executor for nonisolated async
functions (avoiding unnecessary context switches in some edge cases), but that is not applicable in your case.
But this is a digression: In general, if calling something from the main actor and you want to get it off the main thread, a nonisolated
async
function will do the job. (Or, obviously, you could create your own actor
type, and its async
functions will be isolated to that actor, and will not run on the main thread, either.)