Structured Concurrency

InRhythm™
7 min readApr 18, 2023

--

Swift Concurrency

Design Credit: Joel Colletti, Lead UI/UX Designer @ InRhythm

Concurrency is a task-based system that allows developers to take full advantage of the “await” portion of the Async/Await keywords to perform concurrent task execution. An Async/Await task waits until its process finishes before continuing.

Async/Await lets you take advantage of parallelization to make full use of all the available CPU cores to give your users the best user experience.

Swift Concurrency includes, Async/Await, Tasks, and Task Groups. As you read “Async/Await,” assume that it means the entirety of Swift Concurrency and not simply, Async/Await.

Here are the keywords and concepts we’ll use in discussing Structured Concurrency:

  • Task — That portion of the concurrency framework that creates the concurrent environment. It can throw errors and return values
  • Child Task — A task generated and managed by a Parent Task.The Parent is responsible for task execution
  • Cancellation — In Swift, you can cancel or stop any task from completing. Unlike Combine, structured concurrency does not require a strong reference to emit values from the publisher (i.e., the Task) and continue to run even if you remove references to the publisher. If you don’t want the task to run, you must either cancel or stop it
  • Priorities — Structured Concurrency’s Task Priorities are very similar to Apple’s Quality of Service system.

Task Priorities include:

  • High
  • Background
  • Low
  • Medium
  • userInitiated
  • Utility

When using Structured Concurrency in your code, remember that while priority setting in your code tells the system which tasks are most important, those priorities are only a suggestion, not a mandate.

Async/Await

Async/Await provides the Structured Concurrency that removes the necessity of manually propagating errors and managing cancellation, as well as any other types of concurrency issues (e.g., race conditions). It makes program logic simpler to follow as the methods execute linearly rather than with the back and forth required when using completion handlers.

Structured concurrency also improves performance and allows for cleaner, more readable code with more precise error handling. In structured concurrency, long-running tasks are self-managing in regard to resources, error propagation, or any concurrency issues.

Async/Await was first popularized in 2012 via the C# programming language. Between 2012 and 2020, other languages began adopting Async/Await, including but not limited to:

  • Python / Typescript in 2015
  • Kotlin in 2018
  • Rust in 2019
  • C++ in 2020
  • Swift in 2021

Tasks

Although Tasks appear very basic, behind the scenes there is a great deal happening.

The Task keyword tells the compiler that the code inside the brackets must run in its own area using different rules than the rest of the application. This is the beginning of writing concurrent code in Swift.

In Swift, Tasks check if they were cancelled at appropriate times during execution. If cancelled, the Task begins whatever processing is necessary to cease execution and then responds to the cancellation. Depending on the code, the response could be returning partially finished work, returning an empty collection (or nil), or throwing a cancellation error.

Task Example

The load function shown below displays all the parts of a concurrent function coded as a task.

Child Tasks

Child Tasks are bound to a Parent Task and inherit the Parent Task’s Priority. Parent Tasks can have multiple Child Tasks and a Child Task can be a Parent Task to other Child Tasks.

While Parent Tasks can cancel a Child Task, a Child Task can not cancel their Parent task. Child Tasks begin whatever process is necessary for them to halt once their Parent Task is cancelled. All Child Tasks must finish before the Parent Task can be completed.

Cancellations

Unlike Combine, Tasks in Structure Concurrency will continue to run even when there are no Subscribers. Tasks run until they are completed or until you cancel them in code or programmatically. Cancellations are not determined by strong reference.

Tasks “know” whether they’ve been canceled and you can explicitly cancel a Task. You can also use the Task.checkCancellation() call to check if a Task was cancelled.

There are a number ways to programmatically cancel a Task including:

  • Navigating away from a view where the task was triggered
  • Canceling that Task’s Parent Task to cancel all of its Child Tasks
  • When another Child Task of the same Parent throws an Error

Async Let

Async Let is a variation of Async/Await that differs from the Await version, in that it will immediately launch a known number of Tasks in parallel and manage them in a slightly different manner than Async/Await. Where an Async/Await call suspends the Parent Task during Child Task execution, the Async Let call allows the Parent Task to continue running normally.

You would use Async Let when you’re certain that the Async code can run independently. Note that unlike a regular Async/Await Call, you cannot directly cancel Async Let calls.

A further difference is that you can only use Async Let in local declarations, not at the top level.

Use Async Let when you need the results from the call later in your application, not immediately. For example, you could use multiple Async Let calls to load gallery images without blocking the main thread, thereby allowing the user to continue using the application’s GUI while the images load.

Use Async Let when:

  • You have enough detail to request the information you need prior to making the call
  • The calls do not depend on another call’s results
  • You are returning different kinds of data
  • The order in which the app returns results is unimportant

Task Groups

Task Groups allow you to combine an unknown number of parallel Child Task calls that return the same Type of data into a batch, and then wait until the last Child Task finishes before completing the call. This is most useful when the results are a consistent type, such as when combining API responses into a single object. Task Groups only complete after all Child Tasks in the Task Group are complete.

It is important to never mutate the Task Group using code from outside the Task where the Task Group was created. Doing so interleaves data obtained by the Task Group with the data from outside the Task Group which leaves the results inconsistent.

Using Try Await allows you to handle any potential thrown errors in the grouped call using additional code.

For example, let’s say you have a function that returns a string value and you need to call the function n times. Each time that function returns, you want to append the resultant string to an array of strings:

Once all the Tasks in the Task Group are complete, you want the Task Group to return a String array. The “of” type for the Task Group is String because the result of the parallel task is a String and the returning type is an array of Strings

Coding UIImages in Task Groups will appear more familiar. For example, let’s say that you have a list of indeterminate size composed of URLs that point to specific images, and a function that downloads an image from each of those URLs and then adds it to an image array returned from the Task Group.

In this case, the “of” type for your Task Group is UIImage, and because the parallel function inside the task is downloading UIImage data, and the returning type for the Task Group is an array of UIImages.

Task Group Example

The Of type for this example Task Group scenario is UIImage and because the parallel function inside the task is downloading UIImage data, the “returning” type of the task group is an array of UIImages.

There are two things happening in this example code. The first is the execution of the parallel downloading task for the individual images located at each URL. The second takes the results from the previous task (the UIIMage) and combines them into a single array of images.

Resources

Originally published at www.inrhythm.com and written by Mike Adams, Senior Technical Writer @ InRhythm.

--

--

InRhythm™

We are Knowledge Driven, Agile Mobile and Web Consultants. 🚀