I'm confused by these two seemingly contradictory statements about Swift's concurrency model:
At minute 58 of the WWDC session Swift Concurrency Code Along, the speaker says:
Remember this handler is running on the main thread, so when we create a Task that task will also be running on the main thread
At minute 23 of the WWDC session Meet Async/Await, the speaker says:
An async Task packages up the work in the closure and sends it to the system for immediate execution on the next available thread, like the async function on a global dispatch queue
I am having a hard time squaring these two statements. Is a Task
similar to DispatchQueue.global().async
or does it depend on the context?
You asked:
In short,
Task {…}
is not the same asDispatchQueue.global().async
. That second quote is imprecise.A
DispatchQueue.global().async
dispatches to the next available worker thread from the thread pool. ATask {…}
, however, is isolated to the current actor, if any.Task.detached {…}
is more akin to a dispatch to a GCD global queue thanTask {…}
is. (Note, with the explosion ofasync
API, explicitly moving something off the current actor is not nearly as common as it was back in the day. Likewise, if you really need to get something off the actor because it is slow and synchronous, there are a ton of subtle concerns with Swift concurrency.)But, as you suggested, context matters.
Task {…}
behaves much likeTask.detached {…}
if invoked from a nonisolated context. I would suggest, though, that if you must get it off the current actor, then it would be best to avoidTask {…}
and instead useTask.detached {…}
. The resulting behavior is not context-dependent and the developer’s intent is more obvious.FWIW, while the compiler is excellent at verifying a type’s actor-isolation, there are edge cases where it is not remotely obvious to the casual reader. (E.g., add a
@StateObject
property to a nonisolated type and it becomes isolated!) It is best not to write code reliant upon assumptions about a type’s lack of actor-isolation. One can make an argument that if you must get it off the current actor, if any, it might be best to do so explicitly. Likewise, if you do not explicitly need it off the current actor, then do not do so unnecessarily.As an aside, if one really need to get something off the current actor, one might reach for a task group,
async let
, anonisolated
async
function (per SE-0338), or isolate it to a different actor. It is hard to generalize this. We should avoid unnecessary unstructured concurrency.Let us look at the code referenced in that second quote. They
await
a call to the view model’sfetchImage
:Given the
await
, we are not too worried about blocking a thread in this case. The whole idea of a concurrency system is that the current thread is not blocked while it awaitsfetchImage
.In fact, if you change that example to use
Task.detached {…}
, you will get an appropriate compile-time error (if you have “Strict concurrency checking” build setting of “Complete”).FWIW, WWDC 2021 video Swift concurrency: Behind the scenes is a great introduction to the threading model underpinning Swift concurrency.
While I included the code snippet from the video, above, for the sake of completeness, I will take it to the next step and note that we would probably use a
.task
view modifier:In that video, they were walking us through the transition from GCD to Swift concurrency, and that
onAppear
with aTask
was just one step in the transition. But the.task
view modifier takes it a step further, not only dropping us in an asynchronous context, but automatically canceling the task if the user leaves the view before the asynchronous work is done.