12.
Concurrency
Written by Ehab Amer
The code you’ve written in the previous chapters of this book is all synchronous, meaning that it executes statement-by-statement, one step at a time, on what’s known as the main thread. Synchronous code is the most straightforward code to write and reason about, but it comes with a cost. Operations that take time to complete, including reading from a network or database, can stop your program and wait for the operation to finish. For an interactive program such as a mobile app, this is a poor user experience because a great app needs to be fast and responsive.
By executing these operations asynchronously, your program can work on other tasks, such as updating the user interface while it waits for the blocking operation to complete. Working asynchronously introduces concurrency into your code. Your program will work on multiple tasks simultaneously.
Swift has always been capable of using concurrency libraries, such as Apple’s C-language-based Grand Central Dispatch. More recently, the core team has introduced a suite of language-level concurrency features, making it more efficient, safer and less error-prone than ever before.
This chapter gets you started in this new world of concurrency. You’ll learn essential concepts, including:
- How to create unstructured and structured tasks.
- How to perform cooperative task cancellation.
- How to use the
async/awaitpattern. - How to create and use
actorandSendabletypes.
Note: You may have heard of multithreaded programming. Concurrency in operating systems is built on top of threads, but you don’t need to manipulate them directly. In Swift-concurrency-speak, you use the term main actor instead of main thread. Actors are responsible for maintaining the consistency of objects you run concurrently in your program.
Basic Tasks
You’ll start with something super simple: Creating an unstructured task, which is an object that encapsulates some concurrent work. You can do that in an iOS Playground like this:
Task {
print("Doing some work on a task")
}
print("Doing some work on the main actor")
The Task type takes a trailing closure with some work — print a message in this case — to do simultaneously with the main actor. Running this playground prints:
Doing some work on a task
Doing some work on the main actor
Changing the Order
In the example above, the code executed in the order the statements in the playground occurred. To see how that can change, replace the Task with some real work, like this:
Task {
print("Doing some work on a task")
let sum = (1...100).reduce(0, +)
print("1 + 2 + 3 ... 100 = \(sum)")
}
print("Doing some work on the main actor")
The computational details aren’t important; know that it finds the sum of numbers from 1 to 100 and takes a little extra time to complete.
When you print now, notice that the order of the statements has changed:
Doing some work on a task
Doing some work on the main actor
1 + 2 + 3 ... 100 = 5050
And herein lies the fundamental challenge with concurrent programming: The order of events can change depending on the input size, processing power or how the operating system scheduler decides to schedule tasks to work on.
The new features in the Swift language shine in addressing this challenge, adding compiler and API support to make things as easy to reason about as possible.
Canceling a Task
Next, you’ll practice canceling a task. To do this, replace the code with the following:
let task = Task {
print("Doing some work on a task")
let sum = (1...100).reduce(0, +)
try Task.checkCancellation()
print("1 + 2 + 3 ... 100 = \(sum)")
}
print("Doing some work on the main actor")
task.cancel()
This code creates a named variable, task, for the Task and then calls cancel() to cancel it. Also, notice another critical change to the task: the try Task.checkCancellation() statement. This code checks a Boolean flag, Task.isCancelled, and throws an error, causing the task to unwind if a cancellation occurs. It does so in this case, and the output is:
Doing some work on a task
Doing some work on the main actor
The cancellation works as expected, and the sum doesn’t print. The key observation in this example is that it requires some extra work — you need to use checkCancellation() to instruct the program when and how cancellation should happen. This requirement is a concurrency design pattern known as cooperative cancellation.
Suspending a Task
Suppose you want to print the message Hello, wait for a second and then print Goodbye. You’d add this to your playground:
print("Hello")
Task.sleep(for: .seconds(1))
print("Goodbye")
This code seems straightforward. Unfortunately, you get a bunch of errors:
The error message points out two problems:
-
Task.sleep(for:)is an async function. An async function can suspend and resume execution, and you can’t do that unless you are in an asynchronous context. -
Task.sleep(for:)can throw an error, which it needs to do to support cancellation, so you need to usetry,try?ortry!.
Attempt to fix the problem by replacing the code above with this:
Task {
print("Hello")
try Task.sleep(for: .seconds(1))
print("Goodbye")
}
The Task provides the async context. The try recognizes the function can fail.
Adding try mostly fixes the problem, but now you get this:
Clicking the Fix button will give you this code, which will work!
Task {
print("Hello")
try await Task.sleep(for: .seconds(1))
print("Goodbye")
}
Just as the try flags indicate that a function can fail, marking it with await recognizes that it can suspend and resume execution, which is what sleep does.
Wrapping it in a Function
Suppose you want to put that functionality into, well, a function. You might start like this:
func helloPauseGoodbye() {
print("Hello")
try await Task.sleep(for: .seconds(1))
print("Goodbye")
}
This code will produce these errors:
One of those errors should look familiar to you: Functions that try need to either handle the error or be marked with throws. You can fix the other error by pressing the Fix button.
The final fixed code looks like this:
func helloPauseGoodbye() async throws {
print("Hello")
try await Task.sleep(for: .seconds(1))
print("Goodbye")
}
Task {
try await helloPauseGoodbye()
}
The function is marked async and throws. This declaration means that it might throw an error and it might suspend its execution. So, to call this function, you must first mark it with try and then mark it async at the call site. Also, you cannot await a function from the main actor, so you must put this in either a Task or another async function.
Note: There’s a lot of similarity between throwing functions and async functions. You must mark both explicitly in the declaration (
async throws) and at the call site(try await). That’s not an accident! To keep things consistent, you always mark functions withasync throwsin that order. The call site istry await, in that (opposite) order. Don’t worry if you forget the order; type it in, and the compiler fix-it will help you.Of course, it’s possible to have asynchronous functions that don’t throw and throwing synchronous functions.
The Structure of Tasks
You might have heard that Swift implements structured concurrency. That’s because tasks organize themselves into a tree-like structure with parent and child tasks.
Giving concurrency and tasks a structure lets you reason better about operations like ordering and cancellation. It lets the system efficiently allocate operating system threads to handle the list of tasks at hand.
Comment out Task.cancel() and run your playground now; you’ll see something like this:
Doing some work on a task
Doing some work on the main actor
Hello
Hello
1 + 2 + 3 ... 100 = 5050
Goodbye
Goodbye
This mixed output might seem weird, but it’s because you didn’t give your tasks any structure when you created them — everything’s just running at once. Later, you’ll see how to structure tasks.
Decoding an API — Learning Domains
So far, you’ve just seen contrived printing examples. To get more practice, you’ll asynchronously download and decode all of the “learning domains” from Kodeco using the website’s API. This activity will involve:
- Asynchronously fetching data from a URL.
- Decoding the data from JSON into lovely types.
The function will look like this:
func fetchDomains() async throws -> [Domain] {
[] // Fill in the implementation later
}
Take a moment to appreciate the clarity of this function declaration. It tells you that it’s a potentially long process that can suspend and could also fail. On success, it returns a list of Domain values.
Here’s how the API returns the learning domains:
{
"data":[
{
"id":"1",
"type":"domains",
"attributes":{
"name":"iOS \u0026 Swift",
"slug":"ios",
"description":"Learn iOS development with SwiftUI and UIKit",
"level":"production",
"ordinal":1
}
}
]
}
As you see, the JSON hierarchy has three nested levels. Each domain in data has certain attributes. You model the whole thing like this:
struct Domains: Decodable {
let data: [Domain]
}
struct Domain: Decodable {
let attributes: Attributes
}
struct Attributes: Decodable {
let name: String
let description: String
let level: String
}
These types store only the attributes that you care about. The types are Decodable because their properties are Decodable.
Now, it’s time to download domains from the server!
Async/Await in Action
Swift’s concurrency features make asynchronous code nearly as easy to read and write as synchronous code. Here’s how you implement fetchDomains:
func fetchDomains() async throws -> [Domain] {
// 1
let url = URL(string: "https://api.kodeco.com/api/domains")!
// 2
let (data, _) = try await URLSession.shared.data(from: url)
// 3
return try JSONDecoder().decode(Domains.self, from: data).data
}
In this code, you:
- Create a URL to download from. You can use force unwrapping here because this URL string isn’t external, untrusted input, and you can guarantee it’s well-formed.
- Use
URLSession.shared.data(from:)to receive the data and response from the server. This method is asynchronous, so mark its call withawait. This suspension point frees up your program to do other things while waiting for the call to complete. The call also throws errors, so mark it withtry. In addition to data, this method returns a response type, but you can ignore it using an underscore:_. - Decode the received data and return the
Domainstype stored in thedataproperty.
Note: The playground may complain that it cannot find
URL,URLSession, orJSONDecoderin scope. If that happens, addimport Foundationanywhere before the signature of yourfunc fetchDomains()line. The convention is to put imports at the beginning of the file, but as long as it appears before it’s needed, the code will compile correctly. Regularly, Apple seems to change which frameworks get imported by default into playgrounds.
To test the download function, add this code to the playground and run it:
Task { // 1
do { // 2
let domains = try await fetchDomains() // 3
for domain in domains { // 4
let attr = domain.attributes
print("\(attr.name): \(attr.description) - \(attr.level)")
}
} catch {
print(error)
}
}
This code exercises your function by:
- Creating a
Taskcontext you canawait. - Creating a block to
tryandcatcherrors. - Performing the actual download.
awaitrecognizes that the task can suspend here while other things are happening. - Printing out the list of learning domains.
Swift’s concurrency features make downloading data asynchronously a breeze.
Asynchronous Sequences
Another powerful abstraction that Swift concurrency gives you is the asynchronous sequence. Getting each element may cause the task to suspend:
func findTitle(url: URL) async throws -> String? {
for try await line in url.lines {
if line.contains("<title>") {
return line.trimmingCharacters(in: .whitespaces)
}
}
return nil
}
The type URL has a convenience property called lines that returns an asynchronous sequence of strings for each file line. You can loop over this string with the for try await line in url.lines. It can stop loading and return the answer when it hits a line with <title> in it.
To test it, add the following to your playground and run it:
Task {
if let title = try await findTitle(url: URL(string:
"https://www.kodeco.com")!) {
print(title)
}
}
This code will print:
<title>Kodeco | Learn iOS, Android & Flutter</title>
Ordering Your Concurrency
In the previous examples, you made a new unstructured Task block whenever you needed an asynchronous context that could suspend and resume. Suppose you want to get the titles of two web pages.
You can write a function like this:
func findTitlesSerial(first: URL,
second: URL
) async throws -> (String?, String?) {
let title1 = try await findTitle(url: first)
let title2 = try await findTitle(url: second)
return (title1, title2)
}
This async function:
- Attempts to find the first title, then suspends.
- Attempts to find the second title, then suspends.
- Serially returns the result as a tuple.
Since the second title doesn’t depend on the first, processing them in parallel is faster. To do this, you might create two new, unstructured tasks for each findTitle. While this would work, it’s a lot of bookkeeping, especially if you want to support cancellation. You’d need to write code to inform other tasks when one gets canceled.
A better way is to use asynchronous bindings, like this:
func findTitlesParallel(first: URL,
second: URL
) async throws -> (String?, String?) {
async let title1 = findTitle(url: first) // 1
async let title2 = findTitle(url: second) // 2
let titles = try await [title1, title2] // 3
return (titles[0], titles[1]) // 4
}
Here’s what’s happening:
- The declaration
async letspins up a new child task that finds the first title. - The declaration
async letspins up another child task in parallel that finds the second title. -
try awaittakes a sequence of asynchronous tasks and waits for all of them to finish. - The results are returned as a tuple.
The nice thing about making structured tasks this way is that it’s easier to reason about the lifetime and cancellation of tasks. For example, if the parent task that findTitlesParallel(first:second:) is running in gets marked as canceled, the child tasks are automatically marked as canceled.
Asynchronous Properties and Subscripts
Just as you saw with throws in Chapter 5, “Error Handling”, you can mark read-only computed properties with async:
extension Domains {
static var domains: [Domain] {
get async throws {
try await fetchDomains()
}
}
}
You can test it with the following:
Task {
dump(try await Domains.domains)
}
Similarly, you can also create asynchronous read-only subscripts:
extension Domains {
enum Error: Swift.Error { case outOfRange }
static subscript(_ index: Int) -> String {
get async throws {
let domains = try await Self.domains
guard domains.indices.contains(index) else {
throw Error.outOfRange
}
return domains[index].attributes.name
}
}
}
Task {
dump(try await Domains[4]) // "Game Tech"
}
The subscript above is asynchronous and throwable since it uses the previously created computed property to determine the result value.
Note: You may be unfamiliar with the
dumpkeyword. It behaves likedumpis optimized to show structures and objects and uses mirroring to display data.dumpeven has some optional parameters to help keep large, complex objects from polluting your console output.Stringtypes and uses an object’s.descriptionproperty through string interpolation.
Introducing Actors
So far, you’ve seen how to introduce concurrency into your code. However, concurrency isn’t without its risks. In particular, concurrent code can access and mutate the same state simultaneously, causing unpredictable results.
A classic example is a bank account where two people at different ATMs withdraw the entire balance from the same bank account at precisely the same time.
If the code isn’t written carefully, both withdrawals will succeed, which isn’t good news for the bank. Swift concurrency includes the special types actor and Sendable to deal with this consistency issue.
First, consider the following Playlist:
// 1
class Playlist {
let title: String
let author: String
private(set) var songs: [String]
init(title: String, author: String, songs: [String]) {
self.title = title
self.author = author
self.songs = songs
}
func add(song: String) {
songs.append(song)
}
func remove(song: String) {
guard !songs.isEmpty, let index = songs.firstIndex(of: song) else {
return
}
songs.remove(at: index)
}
func move(song: String, from playlist: Playlist) {
playlist.remove(song: song)
add(song: song)
}
func move(song: String, to playlist: Playlist) {
playlist.add(song: song)
remove(song: song)
}
}
This class has four methods that change the state of songs. These methods are not safe to use concurrently. If you made them concurrent, multiple tasks would change the playlist simultaneously, resulting in an unpredictable and inconsistent state. You can solve this problem by converting the class to an actor. Like classes, actors are reference types that represent a shared mutable state. Importantly, actors prevent concurrent access to their state. They allow only one method to access their state at any given time.
Converting a Class to an Actor
Here’s how you convert your Playlist from class to actor:
// 1
actor Playlist {
let title: String
let author: String
private(set) var songs: [String]
init(title: String, author: String, songs: [String]) {
self.title = title
self.author = author
self.songs = songs
}
func add(song: String) {
songs.append(song)
}
func remove(song: String) {
guard !songs.isEmpty, let index = songs.firstIndex(of: song) else {
return
}
songs.remove(at: index)
}
// 3
func move(song: String, from playlist: Playlist) async {
// 2
await playlist.remove(song: song)
add(song: song)
}
func move(song: String, to playlist: Playlist) async {
await playlist.add(song: song)
remove(song: song)
}
}
Here’s what’s changed:
- The keyword
actorreplaces the keywordclass. - Both
move(song:from:)andmove(song:to:)have an additionalPlaylistas a parameter. This parameter means that they operate on two actors:selfandplaylist. You must useawaitto access the otherplaylistbecause the methods may have to wait their turn to get synchronized access to theplaylistactor. - Because
move(song:from:)andmove(song:to:)useawaitin their implementation, you must now mark them asasync. All actor methods are implicitly asynchronous, but the implementation forces you to be explicit here.
Making the Code Concurrent
You can now safely use playlists in concurrent code:
let favorites = Playlist(title: "Favorite songs",
author: "Ehab",
songs: ["Where My Heart Will Take Me"])
let partyPlaylist = Playlist(title: "Party songs",
author: "Ray",
songs: ["Stairway to Heaven"])
Task {
await favorites.move(song: "Stairway to Heaven", from: partyPlaylist)
await favorites.move(song: "Where My Heart Will Take Me", to: partyPlaylist)
await print(favorites.songs)
}
You must use await here to isolate the actor. The requirement to write await makes it evident that the method could suspend if another piece of code is in the middle of accessing the Playlist. The actor guarantees that only one piece of code can access Playlist at any given time, making it safe. Notice that you call add and remove without using await inside the implementation of the move methods. Leaving it out works because the compiler knows you already have exclusive access to the instance.
The actor prepares two internal methods for every method of an actor: One version that needs to await and another fast version that doesn’t. The compiler knows which internal method it needs to call to maximize performance safely.
Using the Noninsulated Keyword
Actors, incidentally, are first-class types and can implement protocols, just like classes, structs and enums do:
extension Playlist: CustomStringConvertible {
nonisolated var description: String {
"\(title) by \(author)."
}
}
print(favorites) // "Favorite songs by Ehab."
Notice the nonisolated keyword. What’s that doing here?
The CustomStringConvertible protocol requires a synchronous description property. However, like actor methods, actor properties are also implicitly asynchronous so they can suspend and wait for other tasks accessing the property to finish. This protection is called actor isolation. Unfortunately, it does not match the protocol definition, which assumes no contention. The nonisolated keyword makes this property synchronous by disabling the actor’s synchronization features.
It’s safe to do that in this case because both title and author are constants. Therefore, the computed property only accesses immutable states.
Sendable
Types conforming to the Sendable protocol are isolated from shared mutations, so they’re safe to use concurrently or across threads. These types have value semantics, which you read about in detail in Chapter 8, “Value Types & Reference Types.” Actors only deal with Sendable types; in future versions of Swift, the compiler will enforce this.
Actors and standard value types like Int and String are Sendable by default. Structures are also Sendable as long as their stored properties are Sendable.
Classes aren’t usually Sendable since they’re reference types, but they can be if you’re careful:
final class BasicPlaylist {
let title: String
let author: String
init(title: String, author: String) {
self.title = title
self.author = author
}
}
extension BasicPlaylist: Sendable {}
Here, BasicPlaylist is Sendable because it’s final, so it doesn’t support inheritance, and all of its stored properties are immutable and Sendable.
Functions and closures can also conform to Sendable:
// 1
func execute(
task: @escaping @Sendable () -> Void,
with priority: TaskPriority? = nil
) {
Task(priority: priority, operation: task)
}
// 2
@Sendable func showRandomNumber() {
let number = Int.random(in: 1...10)
print(number)
}
execute(task: showRandomNumber)
Here’s what happens in the code above:
-
execute(task:with:)runs a task asynchronously with a certain priority. You marktaskasescapingandSendablebecauseinit(priority:operation:)expects an escapingSendableclosure foroperation. Recall from Chapter 7, “Memory Management”, that@escapingis required for closure parameters that you store and use at a later time. -
showRandomNumber()prints a random number between 1 and 10. You make itSendablesinceexecute(task:with:)expects aSendablefunction fortask.
The requirement for Sendable is that the closure does not capture or modify shared mutable state.
Challenges
Here’s a set of challenges to test your concurrency knowledge. It’s best to try and solve them yourself, but solutions are available in the challenges download folder or at the printed book’s source code link in the introduction.
Challenge 1: Safe Teams
Using the above Playlist example as a guide, change the following class to make it safe to use in concurrent contexts:
class Team {
let name: String
let stadium: String
private var players: [String]
init(name: String, stadium: String, players: [String]) {
self.name = name
self.stadium = stadium
self.players = players
}
private func add(player: String) {
players.append(player)
}
private func remove(player: String) {
guard !players.isEmpty, let index = players.firstIndex(of: player) else {
return
}
players.remove(at: index)
}
func buy(player: String, from team: Team) {
team.remove(player: player)
add(player: player)
}
func sell(player: String, to team: Team) {
team.add(player: player)
remove(player: player)
}
}
Challenge 2: Custom Teams
Conform the asynchronous-safe type from the previous challenge to CustomStringConvertible.
Challenge 3: Sendable Teams
Make the following class Sendable:
class BasicTeam {
var name: String
var stadium: String
init(name: String, stadium: String) {
self.name = name
self.stadium = stadium
}
}
Key Points
Concurrent programming is a crucial topic. Future versions of Swift will likely refine the tools and approaches for writing robust concurrent programs.
- The Task type lets you spin up a new task that executes code concurrently.
- Tasks support cancellation but require your cooperation to implement. This explicit checking is called cooperative cancellation.
- Asynchronous functions are marked with async and can suspend and resume after you call them.
- You can only call asynchronous functions in a
Taskcontext or within another async function. You can’t call them from the main actor. - When you call an asynchronous function, you must use
await, recognizing that your program could potentially suspend at that point. - The standard library provides asynchronous sequences whenever getting the next element might require your program to suspend.
- Use for try await in to loop through asynchronous sequences.
- Asynchronous bindings allow you to spin up additional child tasks to work in parallel.
- Swift implements structured concurrency where tasks have a parent-child relationship, making it easier to reason about lifetime and cancellation.
- Read-only computed properties and subscripts can be marked asynchronous in addition to throwing.
- Actors are new reference types in Swift whose primary job is to protect the shared mutable state of the type.
- Sendable types are isolated from mutable shared state changes and can be safely shared between actors in your program.