Swift · · 15 min read

Deep Dive into Swift 5.5's Async/await Concurrency Model

Deep Dive into Swift 5.5's Async/await Concurrency Model

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 blockcompletion 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:

swift-async-await-concurrency

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 blockcompletion 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 call await 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!

Read next