Important terminology nitpick: starting a new task with Task { … }
(or Task.detached
) is not structured concurrency.
Structured concurrency implies creating one or more child tasks by using a task group or async let
. The main benefits of structured concurrency are IMO (from most to least obvious):
-
Cancellation propagation. When a parent task gets canceled, all of its child tasks will automatically get canceled too.
-
Less "spaghettification" of your concurrent code because the visual structure of the code maps directly to the parent-child task relationships. Swift guarantees that all child tasks will have finished by the end of the scope in which they were created. So a child task can never outlive its parent task.
-
It makes it harder to do "fire-and-forget" concurrency.
DispatchQueue.async { … }
is a typical "fire-and-forget" pattern: the calling code doesn't get notified when the async block finishes or errors unless you write the notification glue code manually (by passing in a completion handler).Structured concurrency more or less forces you to inspect the return values or errors from your child tasks. This can make it a bit more unwiedly to actually write structured concurrency code (
Task { … }
is shorter), but the benefits are often worth it.** Important caveat: structured concurrency requires that you are already in an async context – you can't start a child task from a sync context. So sometimes you truly need to start at least one top-level
Task { … }
that can act as the parent for your child tasks. This discussion on Mastodon by @max_desiatov and others has some good pointers how to handle this cleanly.
Here's how a solution with async let
could look like:
@MainActor
func doWorkOnMainActor() -> Int {
return 42
}
func doSomething() async {
async let childTaskResult = {
try await Task.sleep(for: .seconds(2))
return await doWorkOnMainActor()
}()
// Do something else concurrently
// …
// Explicitly wait for the child task to finish.
// If you don't do this, the child task will still be silently awaited
// before the function returns.
do {
print("main actor work finished with \(try await childTaskResult)")
} catch {
print("task was canceled")
}
}
Finally, Task { … }
may still be fine for your use case if you're truly after fire-and-forget semantics. But I'd suggest keeping the distinction between structured and unstructured concurrency in mind, it's super important.