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!
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.
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.
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.
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.
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
.
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.
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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
One Reply to "Concurrency in Swift: Using the new async/await syntax"
Code is incomplete and does not work. You left out implementation of `MyJsonModel`