In designing the async
/await
construct, Apple is hoping to increase the readability, and thus maintainability, of implementing concurrency/parallelism in Swift. What I see in it are attempts:
- to make asynchronous/parallel code almost look like synchronous/sequential code; and,
- to make asynchronous/parallel code much more semantically clear (more readable, less verbose, compact, doing what it says).
These are laudable and ambitious goals, but I’d say Apple’s heading in the right direction. I’m glad because I write a lot a asynchronous code to keep my apps responsive — to create the best possible user experience for my customers. I have a feeling that most of you write a lot of concurrent code, too.
We’ll walk through how to use async
/await
in detail, but I will limit my discussion to it. You need to understand async
/await
before trying to use Swift’s other new concurrency features, like actors, continuations, the AsyncSequence
protocol, the TaskGroup
generic structure, task cancellation, and a few other concepts.
The Swift team has added hundreds of async
(or “awaitable”) methods to the language, but I’d like you to focus on the pattern I use to implement most any call involving async
/await
, not on specific functions. To take an even broader view, I’ll compare using Swift’s async
/await
to using Swift’s Grand Central Dispatch (GCD) implementation. Both technologies offer “generic” support for parallelism. In other words, they both can support asynchronous/parallel code in a wide variety of applications, from long-running multi-step mathematical computations to large file downloads that all need to be run in the background. We’ll go over a common async
/await
beginner’s pitfall and finally end with some advice about where to go next with Swift concurrency.
Please note that async
/await
is not unique to Swift. Javascript, C++, Python, and C# are some examples of languages which support this construct. Also note that while async
/await
is nice to have, I wouldn’t want to give up access to GCD. It’s too powerful, despite its use of closures.
Please download my sample Xcode 13 beta project, written in Swift 5.5, essential for getting the most out of this tutorial. The Base SDK is iOS 15 beta and I used an iPhone 12 Pro Max for the base UI layout in my storyboard.
The heart of the problem
When faced with writing concurrent/asynchronous code, Swift developers are used to submitting a specific task, like downloading a big file, and then waiting for the download to finish in a completion handler or delegate callback. If you review an example of using Apple’s downloadTask(with:completionHandler:)
SDK call that I used in another article right here on AppCoda, you’ll see that the code is a bit awkward, verbose, complex, hard to read, and spread out. Look at where I have to call task.resume()
to start the download. Note that I defined the ImageDownloadCompletionClosure
elsewhere in the code. It’s not super intuitive.
...
func download(completionHanlder: @escaping ImageDownloadCompletionClosure)
{
let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig)
let request = URLRequest(url:imageURL)
let task = session.downloadTask(with: request) { (tempLocalUrl, response, error) in
if let tempLocalUrl = tempLocalUrl, error == nil {
if let statusCode = (response as? HTTPURLResponse)?.statusCode {
let rawImageData = NSData(contentsOf: tempLocalUrl)
completionHanlder(rawImageData!)
print("Successfully downloaded. Status code: \(statusCode)")
}
} else {
print("Error took place while downloading a file. Error description: \(String(describing: error?.localizedDescription))")
}
} // end let task
task.resume()
} // end func download
...
Apple, language architects, and many developers feel that the block
–completion handler
model is hard to read, especially when you’re nesting calls of this type. As Apple puts it:
…Even in [the] simple[st] case, because the code has to be written as a series of completion handlers, you end up writing nested closures. In this style, more complex code with deep nesting can quickly become unwieldy.
So, to nested completion handlers, add error checking, like guard
, if let
, do
, try
, catch
, defer
, throw
, Error
— even NSError
, which also can be nastily nested, you truly get yourself into a major pyramid of doom. Note that my sample project has very little in the way of error checking, not to be sloppy, but so you can concentrate on the subject of this article without distraction.
Enter the Swift 5.5 async
/await
keywords, which by no means constitute all the new Swift concurrency features. I believe you should first learn about async
/await
before exploring the rest of Swift’s new concurrency model.
Good old GCD
Let’s start out talking about a tool that many of us have used in the past to perform long-running operations in the background, thus freeing our apps to do other things, and allowing the user interface (UI) to remain responsive. One common scenario is downloading a background image for an app’s login screen while not preventing user interaction (like typing their username and password). Most of you should know about GCD, but if you don’t, I’ve written an extremely in-depth article on using it with Swift. In fact, the sample code I discussed in that article is used right here.
Let’s first read through my downloadImageGCD
method, part of this article’s companion project. We’ll talk about that function in just a minute:
...
// example of GCD asynchronous code; as
// soon as an image download is queued, control
// flows immediately onwards
func downloadImageGCD(for url: String, with index:Int) -> Void {
DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {
_ = self.isCodeOnMainThread(named: "start GCD image \(index) downloading...")
let imageURL = URL(string: url)
// this call blocks until the image data is downloaded,
// BUT we're running in the background
let imageData = NSData(contentsOf: imageURL!)
// convert the data into an image
let image = UIImage(data: imageData! as Data)
DispatchQueue.main.async {
self.imageViewStellar.image = image
self.imageViewStellar.setNeedsDisplay()
print("GCD image \(index) downloaded/displayed")
}
} // end DispatchQueue.global(qos: ...
} // end func downloadImageGCD
...
By wrapping my GCD code in a for
loop, the downloading of 22 big image files are queued up, but the downloads occur in the background, so the UI remains responsive, and I can click any of my sample app’s buttons at any time and they do some concurrent work. The following for
loop is called when you click the “Start GCD Downloading” button on the app’s main screen:
...
func startGCDAsyncImageDownload()
{
for index in 0..<imageURLs.count
{
downloadImageGCD(for: imageURLs[index], with: index)
}
} // end func startAsyncImageDownload()
...
Watch the video of my app working and check out my print
statements to the console. Notice there’s a bit of stuttering when I click the “Increment” button, but that’s mainly due to updating the UIImageView
on the main thread:
My downloadImageGCD
function is intrinsically asynchronous. Given a URL, you schedule a task to download the data at that URL on a background thread. Then you specify a completion handler to display the downloaded data in a UIImageView
and move on. Code flow whips right through this function. By the time the images start displaying in the UI, you can be off somewhere else in the app’s code. REMEMBER: As shown above, always update your UI on the main thread. See my updateImageView(with:)
used throughout the sample project.
Since I’m running my code on a multi-core processor, calling downloadImageGCD
demonstrates true parallelism. Concurrency/asynchrony involves a stochastic (random) element. Look at my console output to see the random queuing and completion of tasks — and go to Xcode’s Debug navigator -> CPU to see worker threads being created and eventually cleaned up:
start GCD image 0 downloading... ON BACKGROUND THREAD
start GCD image 1 downloading... ON BACKGROUND THREAD
start GCD image 2 downloading... ON BACKGROUND THREAD
start GCD image 4 downloading... ON BACKGROUND THREAD
start GCD image 3 downloading... ON BACKGROUND THREAD
start GCD image 5 downloading... ON BACKGROUND THREAD
...
GCD image 9 downloaded/displayed
GCD image 21 downloaded/displayed
GCD image 7 downloaded/displayed
GCD image 18 downloaded/displayed
GCD image 12 downloaded/displayed
GCD image 5 downloaded/displayed
I purposely used the init(contentsOf:)
initializer of NSData
to download image data because it is a synchronous, blocking call, a concept central to this article. Yes, it blocks until all data at the specified URL downloads, but I’m using that NSData
initializer on a background thread. If you called that function on your main thread, your app would freeze until all URL data downloads. Remember that I submitted the synchronous download inside a block passed to the very asynchronous DispatchQueue...async {...}
. Later, we’ll see that “blocking” or “suspending” have a different meaning in async
/await
.
The SDKs available to Swift have been populated with many, many calls that include completion handlers or delegate callbacks, not just DispatchQueue
. I have a lot of good asynchronous code that’s already been written using the block
–completion handler
pattern. I’m certainly not going to go back and rewrite it all using the new Swift concurrency gimmicks. But looking forward, I will consider using constructs like async
/await
.
The Swift 5.5 async/await construct
Let’s define some terminology essential to understanding Swift’s new concurrency model.
async
:
- add this keyword to a function signature (declaration) to denote that that method is asynchronous, i.e., can potentially run code in parallel with other code, and can be subject to suspension and later resumption
- suspension may occur, for example, to await the results of some long-running task
- suspension may occur, for example, for short-term tasks like updating the UI
- suspension may not occur at all
- functions marked
async
must themselves callawait
to be suspended (see below) - when an async function suspends, it does not block its thread
- when an async function suspends, the operating system can queue and execute other processes on it’s thread
await
:
- obviously marks a potential suspension point in your code, making that suspension point clear to people reading the code
- the code on the rest of the line following the
await
can be scheduled by the system for running a long task - that long task can be suspended once or many times so the system can execute other work
- a shorter task might not be suspended at all and it can run to completion
- once its task finishes, the
await
line finishes, and execution continues on the next line - when Swift suspends the current thread, other code is allowed to execute on that same thread
async-let
:
- lets you call asynchronous functions when you don’t need the return value immediately
- you can start one or many tasks that can be run in parallel
- other code can run while your asynchronous functions process
async {...}
:
- enables you to kick off asynchronous work from within regular, synchronous code, like from within a function not labelled
async
- you call your asynchronous code between the “{” and “}”
How to write code with async/await
Now that we’ve defined the terms async
and await
at a theoretical level, let’s learn how to write code using these constructs. As an alternative to my downloadImageGCD
method, which uses GCD with a completion handler, let’s write an async
function that you call with an image URL and it returns the image data, kind of like a synchronous function would do so. But it doesn’t block, it possibly suspends for a relatively brief period while the image downloads, probably on a background thread, but that is not guaranteed as I understand the new Swift construct (see my definitions above).
...
func downloadImageAsync(for url: String, with index:Int) async throws -> UIImage {
_ = isCodeOnMainThread(named: "start awaitable image \(index) downloading...")
let request = URLRequest(url: URL(string: url)!)
let (data, response) = try await URLSession.shared.data(for: request)
guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw HTTPError.withIdentifier((response as! HTTPURLResponse).statusCode) }
let downloadedImage = UIImage(data: data)
return downloadedImage!
} // end func downloadImageAsync
...
Notice that we place the async
keyword just after the method signature’s parameter list but before the throws
and the return type. Pause for a moment to review my async
keyword definition up above.
The key to this method is this line:
let (data, response) = try await URLSession.shared.data(for: request)
Since URLSession.shared.data
is async
(“awaitable”), we must call it with await
; it also throws
. If an async
function throws
, you always put a try
before the await
, as we did here when calling the function. The lines of code both before and after data(for:)
on the shared URLSession are synchronous. The code after this network download call is dependent on the data
and response
it returns, so it must suspend while the image data at url
downloads. Here’s the magic: while this line (and its containing method) suspend, other work in the app can be done.
A great benefit of writing code with async
/await
is the enhanced readability. You can obviously see asynchronous code and potential suspension points. There’s also less clutter without the closure syntax.
Our first try at async/await
To prove to you that async
/await
works efficiently and allows other app work to be done simultaneously, let’s do an experiment with my code. I’ve got some code commented out, but don’t make any changes yet. Let’s review what you’ll do first. Here’s the code that my app’s “Start Proper Awaitable Downloading” button calls:
...
@IBAction func startProperAwaitableDownloadingButtonClicked(_ sender: Any) {
counter = 0;
counterTextField.text = "0"
imageViewStellar.image = nil
async {
let image = try! await downloadImageAsync(for: imageAltURLs[0], with: 0)
updateImageView(with: image)
print("GIANT FILE FINALLY DOWNLOADED")
//await startRightNewSimpleSyncImageDownload()
//await startRightNewSimpleAsyncImageDownload()
//await startExperimentAsyncImageDownload()
//await startTaskGroupAsyncImageDownload()
}
} // end startProperAwaitableDownloadingButtonClicked
...
While this @IBAction func
is synchronous, note that you can call async
code by wrapping it in the async{...}
construct as defined above. The image at the URL in imageAltURLs[0]
is gigantic and takes awhile to download. You already saw earlier that my downloadImageGCD
function downloads and displays 22 large image files. Ready? Run my app and press the “Start Proper Awaitable Downloading” button, then quickly press the “Start GCD Downloading” button.
Your console output should be similar to that shown next, though not exactly the same. You’ll see the async
call to download the huge file start up and suspend. Then you’ll see the GCD function simultaneously queuing and downloading 22 images. Most of the time, the async
file will download later to last when considering all total 23 downloads, not because there’s anything wrong with async
, but because it’s downloading a very large file.
start awaitable image 0 downloading... ON MAIN THREAD
start GCD image 0 downloading... ON BACKGROUND THREAD
start GCD image 1 downloading... ON BACKGROUND THREAD
start GCD image 2 downloading... ON BACKGROUND THREAD
start GCD image 3 downloading... ON BACKGROUND THREAD
...
start GCD image 19 downloading... ON BACKGROUND THREAD
start GCD image 21 downloading... ON BACKGROUND THREAD
start GCD image 20 downloading... ON BACKGROUND THREAD
GCD image 0 downloaded/displayed
GCD image 4 downloaded/displayed
GCD image 2 downloaded/displayed
...
GCD image 20 downloaded/displayed
GCD image 21 downloaded/displayed
GCD image 16 downloaded/displayed
GIANT FILE FINALLY DOWNLOADED
How NOT to use async/await
Unless you’re enforcing some specific order of execution because of certain interdependencies, I would advise you to avoid using a bunch of await
calls in sequence. Let’s look at two examples from my project. Here’s a “manual” example:
...
// these downloads occur SEQUENTIALLY because we
// start each download with the "await" keyword
func startRightNewSimpleSyncImageDownload() async -> Void {
let stellarImage0 = try! await downloadImageAsync(for: imageURLs[0], with: 0)
let stellarImage1 = try! await downloadImageAsync(for: imageURLs[1], with: 1)
let stellarImage2 = try! await downloadImageAsync(for: imageURLs[2], with: 2)
_ = isCodeOnMainThread(named: "startRightNewAsyncImageDownload")
// no await is needed
updateImageView(with: stellarImage0)
print("stellarImage0 with index 0 downloaded")
updateImageView(with: stellarImage1)
print("stellarImage1 with index 1 downloaded")
updateImageView(with: stellarImage2)
print("stellarImage2 with index 2 downloaded")
} // end func startRightNewSimpleSyncImageDownload
...
Here’s a “automated” example (I used a for
loop):
...
// this method is a misguided attempt to
// call a bunch of blocking calls all at once
func startWrongNewAsyncImageDownload()
{
async {
for index in 0..<imageURLs.count {
let url = imageURLs[index]
let downloadedImage = try! await self.downloadImageAsync(for: url, with: index)
imageViewStellar.image = downloadedImage
print("awaitable image \(index) downloaded/displayed")
}
} // end async
} // end func startWrongNewAsyncImageDownload()
...
Why would I impose such a sequential ordering, especially if I were preparing an image gallery and/or the thumbnails for a UICollectionView
? Even with concurrency involved, think of what would happen if each of the image downloads was in the order of 170 MB, as opposed to the ~30 MB files I’m downloading from my URL array’s addresses. Or suppose I had a long list of mathematically intense calculations? I’d have a lot of uninterruptible tasks that would make my app run sluggishly at best. Even if I were required to enforce a sequence of tasks, each dependent on the previous one, this wouldn’t be the most optimal way to encode my work. See the console output:
start awaitable image 0 downloading... ON MAIN THREAD
awaitable image 0 downloaded/displayed
start awaitable image 1 downloading... ON MAIN THREAD
awaitable image 1 downloaded/displayed
start awaitable image 2 downloading... ON MAIN THREAD
awaitable image 2 downloaded/displayed
start awaitable image 3 downloading... ON MAIN THREAD
awaitable image 3 downloaded/displayed
start awaitable image 4 downloading... ON MAIN THREAD
awaitable image 4 downloaded/displayed
start awaitable image 5 downloading... ON MAIN THREAD
awaitable image 5 downloaded/displayed
start awaitable image 6 downloading... ON MAIN THREAD
awaitable image 6 downloaded/displayed
start awaitable image 7 downloading... ON MAIN THREAD
awaitable image 7 downloaded/displayed
start awaitable image 8 downloading... ON MAIN THREAD
awaitable image 8 downloaded/displayed
start awaitable image 9 downloading... ON MAIN THREAD
awaitable image 9 downloaded/displayed
start awaitable image 10 downloading... ON MAIN THREAD
awaitable image 10 downloaded/displayed
start awaitable image 11 downloading... ON MAIN THREAD
awaitable image 11 downloaded/displayed
start awaitable image 12 downloading... ON MAIN THREAD
awaitable image 12 downloaded/displayed
start awaitable image 13 downloading... ON MAIN THREAD
awaitable image 13 downloaded/displayed
start awaitable image 14 downloading... ON MAIN THREAD
awaitable image 14 downloaded/displayed
start awaitable image 15 downloading... ON MAIN THREAD
awaitable image 15 downloaded/displayed
start awaitable image 16 downloading... ON MAIN THREAD
awaitable image 16 downloaded/displayed
start awaitable image 17 downloading... ON MAIN THREAD
awaitable image 17 downloaded/displayed
start awaitable image 18 downloading... ON MAIN THREAD
awaitable image 18 downloaded/displayed
start awaitable image 19 downloading... ON MAIN THREAD
awaitable image 19 downloaded/displayed
start awaitable image 20 downloading... ON MAIN THREAD
awaitable image 20 downloaded/displayed
start awaitable image 21 downloading... ON MAIN THREAD
awaitable image 21 downloaded/displayed
As we’ll see below, I can use async
/await
to download a whole bunch of images in parallel/simultaneously.
Running multiple parallel tasks with async/await
I always try to learn a new technology myself instead of waiting for someone else to figure it out and then doing copy and paste programming. I like to actually work through a practice test before looking at the answers in the back of the book. I want to figure out why I got some answers wrong.
Such was the case with figuring out how to run multiple parallel tasks with async
/await
. I was able to figure out how to manually use async-let
. When I say “manually,” I mean I could start a fixed number of tasks, but couldn’t figure out how generalize my code for some varying number of tasks. I think the problem with async-let
is that the construct has a let
, implying a constant, not a variable.
Here’s my successful attempt at spawning 3 image downloads in parallel — and displaying the images from the downloaded data:
...
// each download occurs asynchronously -- even simultaneously
func startRightNewSimpleAsyncImageDownload() async -> Void {
async let stellarImage0 = try! downloadImageAsync(for: imageURLs[0], with: 0)
async let stellarImage1 = try! downloadImageAsync(for: imageURLs[1], with: 1)
async let stellarImage2 = try! downloadImageAsync(for: imageURLs[2], with: 2)
_ = isCodeOnMainThread(named: "startRightNewAsyncImageDownload")
updateImageView(with: await stellarImage0)
print("stellarImage0 with index 0 downloaded")
updateImageView(with: await stellarImage1)
print("stellarImage1 with index 1 downloaded")
updateImageView(with: await stellarImage2)
print("stellarImage2 with index 2 downloaded")
} // end func startRightNewSimpleAsyncImageDownload
...
OK, this works, but it is way too specific of a solution. What if I wanted to be able to download 2, 10, 30, or 60 images using some kind of repetitive language construct, like for
or while
? I tried to figure this repetitive thing out for awhile — without looking for somebody else’s solution. Look at my startExperimentAsyncImageDownload()
function for evidence of my failed attempts.
Using a TaskGroup with async/await
In an effort to find a repetitive solution to downloading any number of images, I did some research. All’s I found was a brief section in some Swift documentation with a theoretical and not really usable solution. But I did get something working. Look at my sample code for a method named startTaskGroupAsyncImageDownload()
, study it, run it, look at the console output, and watch my sample app’s screen while the function is working. Here’s the code:
...
// new Swift structured concurrency using
// tasks and task groups
func startTaskGroupAsyncImageDownload() async -> Void {
_ = self.isCodeOnMainThread(named: "startRightNewAsyncImageDownload started")
await withTaskGroup(of: UIImage.self) { taskGroup in
for index in 0..<imageURLs.count {
taskGroup.async {
//_ = await self.isCodeOnMainThread(named: "task for image \(index) downloading started...")
let downloadedImage = try! await self.downloadImageAsync(for: self.imageURLs[index], with: index)
return downloadedImage
}
}
// "To collect the results of tasks that
// were added to the group, you can use
// the following pattern:"
for await image in taskGroup {
_ = self.isCodeOnMainThread(named: "task awaited image downloaded")
// notice I didn't have to jump
// on the main queue to update the UI
self.imageViewStellar.image = image
print("task awaited image displayed")
// DispatchQueue.main.async {
// self.imageViewStellar.image = downloadedImage
// }
}
} // end withTaskGroup
} // end func startTaskGroupAsyncImageDownload()
...
Now this code acts truly concurrent. Hey, wait a minute. We’re using a closure. Hmmm…
I’m not going into a detailed explanation of this method as the main purpose of this article is introducing readers to async
/await
. But let me share one insight. See my comment above this line of code?
for await image in taskGroup {
This was key to figuring out how to make my experimental code work. I found this not on the Swift website, but in the Xcode context sensitive help when I clicked on the withTaskGroup
keyword:
To collect the results of tasks that were added to the group, you can use the following pattern: …
Conclusion
Understanding Swift 5.5’s new concurrency features starts with async
/await
. I hope you learned a lot here today. I encourage you to practice using async
/await
with my sample code, study the links I’ve included in this article, and then to push forward through all the WWDC 2021 presentations on Swift’s other new concurrency features, like on actors, continuations, the AsyncSequence
protocol, the TaskGroup
generic structure, task cancellation, and a few other concepts. I especially recommend the WWDC 2021 sessions on “Explore structured concurrency in Swift”, “Protect mutable state with Swift actors”, and “Swift concurrency: Behind the scenes”.
One bit of homework: Look at my sample code’s isCodeOnMainThread(named:)
function. Can you explain why the function prints “…ON MAIN THREAD” in some places while “…ON BACKGROUND THREAD” in others? I’m sure the Thread.isMainThread
is working properly. I’ll respond to any comments left on my article regarding this question, or respond to any other comments you may have.
Enjoy!