David Fekke I am a software engineer and commercial pilot in Jacksonville, Fl. I also produce videos for the Polyglot Engineer Channel on YouTube.

Concurrency in Swift: Using the new async/await syntax

4 min read 1342

Swift Async Await

When Swift was first introduced by Apple in 2014, it aimed to meet all of the demands that software engineers had for modern programming languages. Chris Lattner, who designed Swift at Apple, had the goal of making a language that could be used both for teaching programming and building software for operating systems.

Since then, Apple has open-sourced the language, and as a result, it continues to evolve. Despite improvements made to Swift, a key feature that is still missing is primitives for concurrency and parallelism.

In the past, you could imitate primitives in Swift using libraries like Grand Central Dispatch (GCD) and libdispatch. Nowadays, we can enforce primitives for concurrency using the async and await keywords.

In this tutorial, we’ll discuss what concurrency is and why it is useful. Then, we’ll learn to use the async and await keywords to enforce concurrency.

Let’s get started!

Concurrency and CPU cores

Due to changes made to processors throughout the last decade, concurrency has become a more relevant topic in computer programming. Despite an increase in the number of transistors in newer processors, there has not been a significant improvement in clock speed.

However, a noteworthy improvement to processors is the presence of more CPU cores on each chip. Apple’s newer processors, like the A14, found in the iPhone 12, have six CPU cores. The M1 processor, used in Macs and the iPad, has eight CPU cores. However, the clock speed for the A14 is still around 3.1 GHz.

The real advancements in CPU design have come from changing the number of cores in modern chips. To take advantage of these newer processors, we need to improve our abilities in concurrent programming.

Long-running tasks

In most modern computer systems, the main thread is used to render and handle the user interface and user interactions. It is often stressed to iOS developers to never block the main thread.

Long-running tasks like making a network request, interacting with a file system, or querying a database can block the main thread, causing the UI of an application to freeze. Thankfully, Apple has provided a number of different tools that we can use to prevent blocking the UI of an application.

We made a custom demo for .
No really. Click here to check it out.

Concurrency options in Swift

Improvements to frameworks like GCD and libdispatch have made concurrent programming much easier.

The current best practice for iOS devices is to offload any task that would block the main thread to a background thread or a queue. Once the task is completed, the results are usually handled in a block or trailing closure.

Before the release of GCD, Apple provided APIs that used delegation to offload tasks. First, a developer had to run a separate thread to a delegated object, which called a method on the calling class to handle the completion of the task.

Although offloading a task works, reading this type of code can be difficult, and any mistakes allow for the introduction of new types of bugs. Therefore, in 2017, Chris Lattner wrote his Swift Concurrency Manifesto, which expressed his ideas on how to add concurrency to Swift using async/await.

Grand Central Dispatch

GCD, first introduced in 2009, is Apple’s method for managing task parallelism through a managed thread pool on Apple’s operating systems.

GCD’s implementation originated as a C library, allowing developers to use it with C, C++, and Objective-C. After Swift was introduced, a Swift wrapper for GCD was created for developers using Apple’s newer language.

GCD has also been ported to libdispatch, which is used in other open source software. The Apache Web Server has incorporated this library for multi-processing.

Grand Central DispatchQueue

Let’s see GCD in action! We’ll use GCD to assign work to another dispatch queue. In the code snippet below, a function is assigning some of its work to an asynchronous task:

swift
func doSomethinginTheBackground() {
    DispatchQueue.global(qos: .background).async {
        // Do some long running work here
        ...
    }
}

The DispatchQueue class provides methods and properties that allow developers to run code in a trailing closure. A common scenario is to run a long-running task in a trailing closure that produces some type of result, and then return that result back to the main thread.

In the code snippet below, the DispatchQueue is doing some work before returning a result back to the main thread:

swift
DispatchQueue.global(qos: .background).async {
    // Do some work here
    DispatchQueue.main.async {
        // return to the main thread.
        print("Work completed and back on the main thread!")
    }
}

A more common scenario would be making a networking call using NSURLSession, handling the results in a trailing closure, and then returning to the main thread:

swift
func goGrabSomething(completion: @escaping (MyJsonModel?, Error?) -> Void) {
    let ourl = URL(string: "https://mydomain.com/api/v1/getsomejsondata")
    if let url = ourl {
        let req = URLRequest(url: url)
        URLSession.shared.dataTask(with: req) { data, _, err in
            guard let data = data, err == nil else {
                return
            }
            do {
                let model = try JSONDecoder().decode(MyJsonModel.self, from: data)
                DispatchQueue.main.async {
                    completion(model, nil)
                }
            } catch {
                completion(nil, error)
            }
        }.resume()
    }
}

Though the example above will compile and run, there are several bugs. For one, we are not using completion handlers everywhere the function can exit. It is also harder to read when writing code synchronously.

To improve on the code above, we’ll use async and await.

Using async/await in your code

When iOS 15 and macOS 12 are released in fall 2021, developers will be able to use the new async/await syntax. You can already use async/await in languages like JavaScript and C#.

These two keywords are becoming the best practice for developers to write concurrent code in modern programming languages. Let’s take a look at the previous function goGrabSomething, rewritten using the new async/await syntax:

swift
func goGrabSomething() async throws -> MyJsonModel? {
    var model: MyJsonModel? = nil
    let ourl = URL(string: "https://mydomain.com/api/v1/getsomejsondata")
    if let url = ourl {
        let req = URLRequest(url: url)
        let (data, _) = try await URLSession.shared.data(for: req)
        model = try JSONDecoder().decode(MyJsonModel.self, from: data)
    }
    return model
}

In the example above, we added the async keyword before throws and after the function name. If our function did not throw, async would go before ->.

I was able to change the function signature so that it no longer requires a completion. Now, we can return the object that has been decoded from our API call.

Inside our function, I am using the keyword await in front of my URLSession.shared.data(for: URLRequest). Since the URLSession data function can throw an error, I have put a try in front of the await keyword.

Every time we use an await in the body of our function, it creates a continuation. If the system has to wait when it processes our function, it can suspend our function until it is ready to return from its suspended state.

If we try to call the goGrabSomething function from synchronous code, it will fail. Swift provides a nice workaround for that use case! We can use an async closure in our synchronous code to call our async functions:

swift
async {
    var myModel = try await goGrabSomething() 
    print("Name: \(myModel.name)")
}

Now, Swift has its own system for managing concurrency and parallelism. By leveraging these new keywords, we can take advantage of the new concurrency features in the system.

The end result is that we are able to write a function that is easier to read and contains less code.

Conclusion

Async/await in Swift greatly simplifies how we write concurrent code in iOS applications. You can play around with these new features by downloading Xcode 13 and running these examples on the beta versions of iOS 15 and macOS 12.

This article scratched just the surface of what is possible with these new features. For example, Swift also has added an actor object type that allows developers to create write objects that contain shared mutable state, which can be used across threads without having race conditions.

I hope you enjoyed this article. If you’re interested in learning more about async/await in Swift, please watch Apple’s WWDC21 presentation.

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
David Fekke I am a software engineer and commercial pilot in Jacksonville, Fl. I also produce videos for the Polyglot Engineer Channel on YouTube.

Leave a Reply