This article is part of my Modern Concurrency in Swift article series.
This article was originally written creating examples using Xcode 13 beta 1. The article, code samples, and provided sample project have been updated for Xcode 13 beta 3.
Table of Contents
- Modern Concurrency in Swift: Introduction
- Understanding async/await in Swift
- Converting closure-based code into async/await in Swift
- Structured Concurrency in Swift: Using async let
- Structured Concurrency With Group Tasks in Swift
- Introduction to Unstructured Concurrency in Swift
- Unstructured Concurrency With Detached Tasks in Swift
- Understanding Actors in the New Concurrency Model in Swift
- @MainActor and Global Actors in Swift
- Sharing Data Across Tasks with the @TaskLocal property wrapper in the new Swift Concurrency Model
- Using AsyncSequence in Swift
- Modern Swift Concurrency Summary, Cheatsheet, and Thanks
Before you try to dive in with concurrency in Swift, you need to understand async/await. There’s no way around it. While async/await are not the only concurrency options, Apple’s SDKs are starting to make heavy use of them. There is no doubt that third-party library providers will start offering these as well.
This article will explore async/await and nothing else. Once you understand these concepts, we will start moving on to more advanced articles where we cover structured concurrency, unstructured concurrency, SwiftUI, and more.
If you have been writing callback-based concurrency, keep in mind that the implementation for async/await is very different from anything you have seen before in Apple’s technologies. It basically throws what you know about concurrent programming out the window. It’s important to keep that in mind as you read this article.
In this article, we will write a function that downloads an image and then its metadata using a different network call. We will show you how doing this with callback-based concurrency can become hard to manage quickly, and how async/await solves this problem beautifully.
Refreshing Concepts
A Refresher on Procedural Programming
When you write any normal program with no exceptional needs such as networking and/or I/O, your program executes in the order your code is written, calling procedures as needed, and returning content to the caller if necessary.
Consider the following code:
func sayHi() {
print("Hi")
}
func multiply(_ x: Int, _ y: Int) -> Int {
x * y
}
func sayBye(result: Int) {
print("Bye \(result)")
}
func performCoolStuff() {
sayHi()
let x = 10
let y = 5
let result = multiply(x, y)
sayBye(result: result)
}
// Calling performCoolStuff()
performCoolStuff()
When you call performCoolStuff()
, your code is executed as follows:
- It will first call
sayHi()
- It will declare two variables,
x
andy
. - It will call
multiply
passing in the values forx
andy
. - It will call
sayBye
with the result of the multiplication
There’s nowhere to get lost here. Your code is called in the same order it was given. Functions that call other functions are placed in the call stack exactly as they appear, unwinding back as they return values to the main callers. As calls happen, the function give back control to the caller through the use of return
. When we call multiply
, we assign control to it, and when it returns us a result, it gives us back control through return
.
You don’t think much about procedural programming. Chances are you do it daily, and it always works the way you expect it to *.
A refresher on callback-based concurrency code.
Things are a bit more complicated when it comes to code that may run in parallel with other code. Consider the following example that will download an image through a network call and the metadata through a different network call (you can copy and paste this code in a view controller of a new project - it contains everything you need to run it). The download takes place at the same time as the main thread’s execution:
struct ImageMetadata: Codable {
let name: String
let firstAppearance: String
let year: Int
}
struct DetailedImage {
let image: UIImage
let metadata: ImageMetadata
}
enum ImageDownloadError: Error {
case badImage
case invalidMetadata
}
func sayHi() {
print("Hi")
}
func multiply(_ x: Int, _ y: Int) -> Int {
x * y
}
func sayBye(result: Int) {
print("Bye \(result)")
}
func downloadImageAndMetadata(
imageNumber: Int,
completionHandler: @escaping (_ image: DetailedImage?, _ error: Error?) -> Void
) {
let imageUrl = URL(string: "https://www.andyibanez.com/fairesepages.github.io/tutorials/async-await/part1/\(imageNumber).png")!
let imageTask = URLSession.shared.dataTask(with: imageUrl) { data, response, error in
guard let data = data, let image = UIImage(data: data), (response as? HTTPURLResponse)?.statusCode == 200 else {
completionHandler(nil, ImageDownloadError.badImage)
return
}
let metadataUrl = URL(string: "https://www.andyibanez.com/fairesepages.github.io/tutorials/async-await/part1/\(imageNumber).json")!
let metadataTask = URLSession.shared.dataTask(with: metadataUrl) { data, response, error in
guard let data = data, let metadata = try? JSONDecoder().decode(ImageMetadata.self, from: data), (response as? HTTPURLResponse)?.statusCode == 200 else {
completionHandler(nil, ImageDownloadError.invalidMetadata)
return
}
let detailedImage = DetailedImage(image: image, metadata: metadata)
completionHandler(detailedImage, nil)
}
metadataTask.resume()
}
imageTask.resume()
}
func performMessyStuff() {
sayHi()
let x = 10
downloadImageAndMetadata(imageNumber: 1) { image, error in
DispatchQueue.main.async {
print("We got results")
}
}
let y = 5
let result = multiply(x, y)
sayBye(result: result)
}
performMessyStuff()
Note: Apple used a similar example in the Meet async/await in Swift session at WWDC2021. This example is based on that, but I created a compilable version you can use.
This is what happens:
- The method calls
sayHi()
normally. - We create a variable
x
and assign it a value. downloadImageAndMetadata
is called, which internally will set up the first variables it needs for its execution (imageUrl
).- We create a variable, once again synchronously, that will hold a
dataTask
and provide it with a completion handler that will be called after it’s done downloading. - We call
resume()
on the task to begin the download. - The contents of the completion handler will not be executed immediately. Instead, while the downloads happen, the program continues its execution.
- The program may, or may not, print
"We got results"
. In the case of a network download, it will always take a while, but if this were a faster asynchronous operation, it may be called at this point. The program will create a variabley
. - If both downloads have finished successfully, the program may print
"We got results".
Otherwise it creates theresult
variable and callsmultiply
, which may or may not finish before the downloads. - If the downloads have finished successfully, the program will print
"We got results"
, otherwise it will callsayBye
. - Somewhere above there and at any point, the program may start the metadata download task after the image task has downloaded.
This flow of execution is messy, because downloading data from the network is asynchronous and all its work happens somewhere else. Anything else may happen on the main thread while the downloads take place. Whatever the console prints may have a different output on each run **. The downloads spawn from the main thread onto another thread, but the program will continue executing the code in the main thread without any issue. This makes it hard to think procedurally, because we rely on the completionHandler
to let us know when it has finished its work. If there are tasks that can be performed in the main thread, but they depend on an image and/or its metadata, we have to do all that work in the completion handler (while rerouting the work to the main thread with DispatchQueue.main.async
whenever relevant).
In the case of callback-based asynchronous code, control is given back whenever completion handlers are executed.
And as you may imagine, these calls can become more and more complex and nested.
Introducing async/await
If I had to explain async/await in few words, I’d say this:
async/await is like a hybrid between procedural programming and callback-based closures.
Before we explain why, let’s keep two things in mind:
- Procedural code runs from top to bottom. Control is given back to the caller through
return
- Callback-based concurrency will create asynchronous tasks, but it will continue executing the current thread without an issue, even if those tasks are running. Control is given back to the caller through completion handlers.
Let’s take a few minutes to discuss the async
and await
keywords individually.
async
async
has two uses:
- To tell the compiler when a piece of code is asynchronous.
- To spawn asynchronous tasks in parallel.
To mark a function as async
, simply put the keyword after the function’s closing parenthesis and before the arrow, like this:
func downloadImage(id: Int) async -> UIImage? { ... }
Or:
func downloadImage(id: Int) async throws -> UIImage { ... }
You can already see a huge advantage here. The completion handler is gone, and our function signature is very clear with its purpose. We can tell at first glance if it is async
and what it returns.
async
code can only run in concurrent contexts. That is to say, within other async
functions, or when manually dispatched via Task {}
. We will explore Task {}
in a bit.
await
await
is where the magic happens. Whenever the program finds the await
keywords, it has the option of suspending the function. It may or may not do so, and the decision is up to the system.
If the system does suspend the function, await
will return control, not to the caller, but to the system. The system will then use the thread to perform other work until the suspended function is done. The statements below await
will not be executed until it has finished. The system decides what’s important to execute, and at some point, it will return control back to you after it sees the await
ed function has finished.
You can think of it as a traffic light. If you are driving down the road and you find a red light, chances are you will stop. But if it is 4 AM in the morning and there’s no cars coming you may just run it. ***
What you need to understand about await is that, if it does choose to suspend, nothing below it will execute until the system tells it to, and the system will use the thread to do other work.
Every call to an async
function, must be marked as await
.
To better understand this, we will rewrite our downloadImageAndMetadata
function, this time using async
and using await
within the body.
func downloadImageAndMetadata(imageNumber: Int) async throws -> DetailedImage {
// Attempt to download the image first.
let imageUrl = URL(string: "https://www.andyibanez.com/fairesepages.github.io/tutorials/async-await/part1/\(imageNumber).png")!
let imageRequest = URLRequest(url: imageUrl)
let (imageData, imageResponse) = try await URLSession.shared.data(for: imageRequest)
guard let image = UIImage(data: imageData), (imageResponse as? HTTPURLResponse)?.statusCode == 200 else {
throw ImageDownloadError.badImage
}
// If there were no issues, continue downloading the metadata.
let metadataUrl = URL(string: "https://www.andyibanez.com/fairesepages.github.io/tutorials/async-await/part1/\(imageNumber).json")!
let metadataRequest = URLRequest(url: metadataUrl)
let (metadataData, metadataResponse) = try await URLSession.shared.data(for: metadataRequest)
guard (metadataResponse as? HTTPURLResponse)?.statusCode == 200 else {
throw ImageDownloadError.invalidMetadata
}
let detailedImage = DetailedImage(image: image, metadata: try JSONDecoder().decode(ImageMetadata.self, from: metadataData))
return detailedImage
}
This is a long function, but it’s already much better than the pyramid version of it. Let’s highlight the important parts first:
- The program procedurally creates
imageUrl
andimageRequest
. - The program reaches a call to an async call,
URLSession.shared.data(for:)
. - The program will make a decision on suspending the function or continuing it. In this case, it’s likely it will suspend due to the nature of networking, but don’t get used to taking that for granted. We will assume the program suspends the function.
- This will give control back to the system.
- The system may do other work that is not relevant to this task while the download
await
s. - Anything under the first await will not be executed. It will not reach the guard, it will not create the variables for the metadata, it will do nothing until the
await
ed function finishes. - After some time, the system will give control back to you, after the
await
ed function has finished. - The
guard
statement is reached, throwing an error if necessary. - The program will repeat steps 2-8 but for the metadata task.
- The program will return a new
DetailedImage
.
As you can see, it is a pretty linear flow, and the way await
suspends the rest of the execution until the system deems it necessary makes it behave very much like procedural programming.
We can separate that function into different functions as well:
func downloadImageAndMetadata(imageNumber: Int) async throws -> DetailedImage {
let image = try await downloadImage(imageNumber: imageNumber)
let metadata = try await downloadMetadata(for: imageNumber)
return DetailedImage(image: image, metadata: metadata)
}
func downloadImage(imageNumber: Int) async throws -> UIImage {
let imageUrl = URL(string: "https://www.andyibanez.com/fairesepages.github.io/tutorials/async-await/part1/\(imageNumber).png")!
let imageRequest = URLRequest(url: imageUrl)
let (data, imageResponse) = try await URLSession.shared.data(for: imageRequest)
guard let image = UIImage(data: data), (imageResponse as? HTTPURLResponse)?.statusCode == 200 else {
throw ImageDownloadError.badImage
}
return image
}
func downloadMetadata(for id: Int) async throws -> ImageMetadata {
let metadataUrl = URL(string: "https://www.andyibanez.com/fairesepages.github.io/tutorials/async-await/part1/\(id).json")!
let metadataRequest = URLRequest(url: metadataUrl)
let (data, metadataResponse) = try await URLSession.shared.data(for: metadataRequest)
guard (metadataResponse as? HTTPURLResponse)?.statusCode == 200 else {
throw ImageDownloadError.invalidMetadata
}
return try JSONDecoder().decode(ImageMetadata.self, from: data)
}
As long as we mark the functions as async
, this is possible to do.
It is important to note the linearity of this. The metadata and image are not being downloaded at the same time. It will download the image first, and the metadata later. We can make it download both the image and metadata at the same time, but this article is not about actual concurrency just yet. We will explore how to do both tasks at the same time when we learn about structured concurrency.
If you want to see the function suspension in action, simply put some print statements before and after await
code. You will see that the print statements will be executed slowly, as the system suspends the download tasks, perform other tasks, and gives control back to you.
func downloadImageAndMetadata(imageNumber: Int) async throws -> DetailedImage {
print("Will download image")
let image = try await downloadImage(imageNumber: imageNumber)
print("Has downloaded image")
print("Will download metadata")
let metadata = try await downloadMetadata(for: imageNumber)
print("Has downloaded metadata")
return DetailedImage(image: image, metadata: metadata)
}
If your internet is a bit too speedy to appreciate the slow prints, Apple provides us a neat method: Task.sleep
. This function solely exists to sleep the thread for a given number of time, and you can use it to explore async/await.
- Note: Unfortunately
Task.sleep
appears to crash as of Xcode 13 Beta 1.await Task.sleep(2 * 1_000_000_000)
.
One final important note about await
: It’s not guaranteed that the same thread that executed the code above it is the same one that will execute the code below it (commonly called the continuation). This has important implications when dealing with UI. If you use await
in a context that needs the main thread such as a ViewController, make sure you mark the functions with await
with the @MainActor
attribute, or add the attribute to the entire class declaration. If you want a complete tour behind how the new concurrency works in Swift, check out the Swift concurrency: Behind the scenes WWDC2021 session talk.
“Bridging” between the sync and async worlds with Task
We can create a “bridge” between the sync and async worlds creating a Task
. To understand why this is necessary, consider the following piece of code:
func performDownload() {
let imageDetail = try? await downloadMetadata(for: 1)
}
The compiler will protect us from erroneously running this, showing the following error:
‘async’ call in a function that does not support concurrency Add ‘async’ to function ‘performDownload()’ to make it asynchronous
The compiler is suggesting we mark performDownload
as async.
func performDownload() async {
let imageDetail = try? await downloadMetadata(for: 1)
}
But this is not always possible. What if performDownload
is in a view controller or in another place that can’t give you an asynchronous context?
To fix this, we can bridge this synchronous function to the asynchronous world using Task {}
.
func performDownload() {
Task {
let imageDetail = try? await downloadMetadata(for: 1)
}
}
We are explicitly creating an asynchronous context, and it will behave as such. We can now call perform download from any sync context without an issue.
get async
To make things even better, properties that are read-only can be await
ed.
Suppose you have the following wrapper object:
struct Character {
let id: Int
}
We can get its image and metadata by calling downloadImageAndMetadata
, but you could also give this object two calculated properties to get its image and/or metadata independently.
struct Character {
let id: Int
var metadata: ImageMetadata {
get async throws {
let metadata = try await downloadMetadata(for: id)
return metadata
}
}
var image: UIImage {
get async throws {
return try await downloadImage(imageNumber: id)
}
}
}
And we can use it as such:
let metadata = try? await character.metadata
Summary
This was a long introduction to async/await, but hopefully the included examples and discussion will help you understand how this works. async/await
are the heart of the new concurrency system, so you need to have a fine grasp of them. Future articles may not be as long. Generally, covering the basics of something requires a lot of effort as it’s important to not miss any details. Hopefully this article will be of use to you.
I have created a sample project that makes use of the downloaded image and metadata in a UIKit project. You can download it from here.
When ran, the program will simply download the contents and display them like this:
On the viewDidAppear
method, you will find the following code:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// MARK: METHOD 1 - Using Async/Await
Task {
if let imageDetail = try? await downloadImageAndMetadata(imageNumber: 1) {
self.imageView.image = imageDetail.image
self.metadata.text = "\(imageDetail.metadata.name) (\(imageDetail.metadata.firstAppearance) - \(imageDetail.metadata.year))"
}
}
// MARK: METHOD 2 - Using async properties
// Task {
// let character = Character(id: 1)
// if
// let metadata = try? await character.metadata,
// let image = try? await character.image{
// imageView.image = image
// self.metadata.text = "\(metadata.name) (\(metadata.firstAppearance) - \(metadata.year))"
// }
// }
// MARK: Method 3 - Using Callbacks
// downloadImageAndMetadata(imageNumber: 1) { imageDetail, error in
// DispatchQueue.main.async {
// if let imageDetail = imageDetail {
// self.imageView.image = imageDetail.image
// self.metadata.text = "\(imageDetail.metadata.name) (\(imageDetail.metadata.firstAppearance) - \(imageDetail.metadata.year))"
// }
// }
// }
}
You can comment and uncomment everything under MARK: - Method x
to fill the outlets with the data provided by the different methods of getting the data. Hopefully you can play around with this to get a better hang of how async/await
work in Swift.
I want to revisit these two points I made earlier:
- Procedural code runs from top to bottom. Control is given back to the caller through
return
- Callback-based concurrency will create asynchronous tasks, but it will continue executing the current thread without an issue, even if those tasks are running. Control is given back to the caller through completion handlers.
We can now append one more thing to summarize:
3 async/await will run in order just like procedural programming. When it finds an
await
call, the job will suspend and will give control back to the system instead of the caller. Unlike callback-based concurrency, it will not continue execution of the statements under it until it has finished. The system will make use of the thread to perform other work, and when it decides it’s time to revisit your function, it will, and execution will resume linearly.
When you are ready, you can proceed to the third article in the series, Converting Closure-based code into async/await in Swift to learn more about continuations, explicit continuations, and how to bridge closure-based and delegate-based code into async/await.
Notes
*: Well, except when you put bugs in.
**: This is not obvious in this example, but there exists asynchronous code that is much faster than a network call and it may finish much faster than expected, altering the output of the console on each run.
***: Drive responsibly.