[WWDC 23] Beyond the basics of structured Concurrency

suojae·2023년 12월 26일
0

Task hierarchy

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

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 or checkCancellation 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 where withTaskCancellationHandler 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.



Task priority

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.



Task group patterns

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.



Task-local values

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.



Task traces

profile
Hi 👋🏻 I'm an iOS Developer who loves to read🤓

0개의 댓글