Hello everyone!
I’ve been diving a bit into the internals of Task.detached lately, since I’ve seen several people using it in their projects, but for reasons that were a bit unclear to me.
As a reminder, a Task is a unit of asynchronous work that inherits the context from where it’s created. By context, I mean 3 things:
- globalActor isolation / SerialExecutor
- Task priorities
- TaskLocal
From the documentation:
The new task defaults to running with the same actor isolation, priority, and task-local state as the current task
Naturally, Task.detached doesn’t inherit any of that:
To create an unstructured task that’s more independent from the surrounding code, known more specifically as a detached task, call the Task.detached(name:priority:operation:) static method. The new task defaults to running without any actor isolation and doesn’t inherit the current task’s priority or task-local state.
Most people use Task.detached specifically to avoid inheriting the isolation / SerialExecutor of the MainActor.
However, since iOS 18, a new API called TaskExecutor has been introduced. Tasks now have a new parameter that allows us to specify the executor on which we want them to run. One of the options is the globalConcurrentExecutor. The documentation states:
You may pass this executor explicitly to a Task initializer as a task executor preference, in order to ensure and document that task be executed on the global executor, instead of e.g. inheriting the enclosing actor’s executor.
I really like this approach because it makes the intent much clearer: we explicitly declare that we prefer to run the task on a specific executor, namely, the Swift Concurrency background pool (the GCE).
So, if it’s recommended to use the globalConcurrentExecutor in the Task initializer to switch to the GCE, then Task.detached really only has two remaining uses
-
Not inheriting the task’s priority
-
Not inheriting TaskLocal values
For the first one, I still don’t quite understand the use case. If you want to change the priority while switching to the GCE, you can just do:
@MainActor
class MainActorClass {
func mainActorMethod() {
Task(executorPreference: globalConcurrentExecutor, priority: .medium) {
}
}
}
PS: The documentation notes that executorPreference won’t work if a custom executor is created, but these are very specific cases.
Also, I didn’t mention using Task.detached to run multiple tasks in parallel, since that’s exactly what structured concurrency is designed for, something Apple also points out in the documentation:
Don’t use a detached unstructured task if it’s possible to model the operation using structured concurrency features like child tasks. Child tasks inherit the parent task’s priority and task-local storage, and canceling a parent task automatically cancels all of its child tasks. You need to handle these considerations manually with a detached task.
So in conclusion, Task.detached is really only useful to avoid inheriting TaskLocal values.
Did I miss anything in my reasoning?