[WWDC 21] Explore Structured Concurrency in Swift

suojae·2023년 12월 23일
0

Async-let tasks

In the early days of computing, programs were hard to read because they were written as a sequence of instructions,
where control-flow was allowed to jump all over the place.

You don’t see that today, because languages use structured programming to make control-flow more uniform.

This also means that the lifetime of any variables defined in a block will end when leaving the block. So, structured programming with static scope makes control-flow and variable lifetime easy to understand.

More generally, structured control-flow can be sequenced and nested together naturally. This lets you read your entire program top to bottom. So, those are the fundamentals of structured programming.

This code does that work asynchronously, taking in a collection of strings that identify the images.

This pattern allows the caller to receive an answer at a later time. As a consequence of that pattern, this function cannot use structured control-flow for error handling. That’s because it only makes sense to handle errors thrown out of a function, not into one.

Also, this pattern prevents you from using a loop to process each thumbnail. Recursion is required, because the code that runs after the function completes must be nested within the handler.

I can now loop over the thumbnails to process them sequentially. I can also throw and catch errors, and the compiler will check that I didn’t forget.

Notice that there is only one flow of execution here, as traced by the arrows through each step

Since the download could take a while, you want the program to start downloading the data and keep doing other work until the data is actually needed. To achieve this, you can just add the word async in front of an existing let binding.

Tasks are a new feature in Swift that work hand-in-hand with async functions. A task provides a fresh execution context to run asynchronous code. Each task runs concurrently with respect to other execution contexts. They will be automatically scheduled to run in parallel when it is safe and efficient to do so.

Every task represents an execution context for your program, two arrows will simultaneously come out of this step. This first arrow is for the child task, which will immediately begin downloading the data. The second arrow is for the parent task, which will immediately bind the variable result to a placeholder value.

Upon reaching an expression that needs the actual value of the result, the parent will await the completion of the child task, which will fulfill the placeholder for result.

Using these concurrently bound variables does not require a method call or any other changes. Those variables have the same type that they did in a sequential binding.

When creating a new structured task like with async-let, it becomes the child of the task that the current function is running on. Tasks are not the child of a specific function, but their lifetime may be scoped to it. The tree is made up of links between each parent and its child tasks. A link enforces a rule that says a parent task can only finish its work if all of its child tasks have finished. This rule holds even in the face of abnormal control-flow which would prevent a child task from being awaited.

In fact, when a task is canceled, all subtasks that are decedents of that task will be automatically canceled too. So if the implementation of URLSession created its own structured tasks to download the image, those tasks will be marked for cancellation.

The function fetchOneThumbnail finally exits by throwing the error once all of the structured tasks it created directly or indirectly have finished. This guarantee is fundamental to structured concurrency.

This guarantee is fundamental to structured concurrency. It prevents you from accidentally leaking tasks by helping you manage their lifetimes, much like how ARC automatically manages the lifetime of memory.



Group tasks

For each thumbnail ID in the loop, we call fetchOneThumbnail to process it, which creates exactly two child tasks. Even if we in-lined the body of that function into this loop, the amount of concurrency will not change. Async-let is scoped like a variable binding.

That means the two child tasks must complete before the next loop iteration begins. But what if we want this loop to kick off tasks to fetch all of the thumbnails concurrently?

A task group is a form of structured concurrency that is designed to provide a dynamic amount of concurrency. Tasks added to a group cannot outlive the scope of the block in which the group is defined.

When the group object goes out of scope, the completion of all tasks within it will be implicitly awaited. This is a consequence of the task tree rule I described earlier, because group tasks are structured too. At this point, we’ve already achieved the concurrency that we wanted: one task for each call to fetchOneThumbnail, which itself will create two more tasks using async-let.

This is a common mistake when increasing the amount of concurrency in your program. Data races are accidentally created.
This dictionary cannot handle more than one access at a time, and if two child tasks tried to insert thumbnails simultaneously, that could cause a crash or data corruption.


Unstructured Tasks

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

0개의 댓글