Structured tasks are created using
async let
and task groups, while unstructured tasks are created using Task and Task.detached. Structured tasks live to the end of the scope where they are declared, like local variables, and are automatically cancelled when they go out of scope, making it clear how long the task will live.
Whenever possible, prefer structured Tasks. The benefits of structured concurrency discussed later do not always apply to unstructured tasks.
Suppose we have a kitchen with multiple chefs preparing soup. Soup preparation consists of multiple steps. Some tasks can be performed in parallel, while others must be done in a specific order.
While this expresses which tasks can run concurrently and which cannot, this is not the recommended way to use concurrency in Swift.
Here is the same function expressed using structured concurrency. Since we have a known number of child tasks to create, we can use the convenient
async let
syntax
This parent-child hierarchy forms a tree, the task tree.
Task cancellation is used to signal that the app no longer needs the result of a task and the task should stop and either return a partial result or throw an error.
In our soup example, we may want to stop making a soup order if that customer left, decided they wanted to order something else, or it's closing time.
Structured tasks are cancelled implicitly when they go out of scope, though you can call "cancelAll" on task groups to cancel all active children and any future child tasks.
Unstructured tasks are cancelled explicitly with the "cancel" function. Cancelling the parent task results in the cancellation of all child tasks.
Cancellation is cooperative, so child tasks aren't immediately stopped. Cancellation is a race. If the task is cancelled before our check, "makeSoup" throws a "SoupCancellationError".
If the task is cancelled after the guard executes, the program will carry on preparing the soup
If we are going to throw a cancellation error instead of returning a partial result, we can call "Task.checkCancellation", which throws a "CancellationError" if the task was cancelled.
It's important to check the task cancellation status before starting any expensive work to verify that the result is still necessary.
Polling for cancellation with
isCancelled
orcheckCancellation
is useful when the task is running, but there are times when you may need to respond to cancellation while the task is suspended and no code is running, like when implementing an AsyncSequence. This is wherewithTaskCancellationHandler
is useful.
In one cancellation scenario, the asynchronous for-loop gets a new order before it is cancelled. The "makeSoup" function handles the cancellation as we defined earlier, and throws an error.
In another scenario, the cancellation may take place while the task is suspended, waiting on the next order. we have to use the cancellation handler to detect the cancellation event and break out of the asynchronous for-loop.
These mechanisms allow us to synchronize the shared state, avoiding race conditions, while allowing us to cancel the running state machine without introducing an unstructured task in the cancellation handler.
Remember, cancellation does not stop a task from running, it only signals to the task that it has been cancelled and should stop running as soon a possible.
First, what is priority, and why do we care? Priority is your way to communicate to the system how urgent a given task is. Certain tasks, like responding to a button press, need to run immediately or the app will appear frozen.
Second, what is a priority inversion? A priority inversion happens when a high-priority task is waiting on the result of a lower-priority task.
When they await their soup, the priority of all child tasks is escalated, ensuring that no high-priority task is waiting on a lower-priority task, avoiding the priority inversion.
The task keeps the escalated priority for the remainder of its lifetime.
If we chop too many ingredients simultaneously, we'll run out of space for other tasks, so we want to limit the number of ingredients getting chopped at the same time.
We replace the original loop over each ingredient with a loop that starts up to the maximum number of chopping tasks.
The new loop waits until one of the running tasks finish and, while there are still ingredients to chop, adds a new task to chop the next ingredient.
Discarding task groups autimatically clean up their children, so there is no need to explicitly cancel the group and clean up.
A task-local value is a piece of data associated with a given task, or more precisely a task hierarchy.
It's like a global variable, but the balue bound to the task-local value is only available from the current task hierarchy.
Task-local values are declared as static properties with the "TaskLocal" property wrapper.