Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

12. Concurrency

Swift’s concurrency has two halves. One is async/await, which lets a function wait for something without blocking a thread, and reads like ordinary sequential code. The other is a compile-time check that no two pieces of code can touch the same mutable state at the same time. The first half is pleasant. The second half is why Swift 6 exists, and it is the half that will argue with you.

12.1 async and await

A function that waits for something is marked async, and calling one is marked await. The await is a suspension point: the function gives up its thread there and resumes when the answer arrives, and nothing else in your function runs in between.

// async let starts the work now and waits at the first use of the result.
func slowSquare(_ n: Int) async -> Int {
    try? await Task.sleep(for: .milliseconds(100))
    return n * n
}

func sequential() async -> Int {
    var total = 0
    for n in 1...3 {
        total += await slowSquare(n)
    }
    return total
}

func concurrent() async -> Int {
    async let a = slowSquare(1)
    async let b = slowSquare(2)
    async let c = slowSquare(3)
    return await a + b + c
}

let clock = ContinuousClock()

let oneAfterAnother = await clock.measure { _ = await sequential() }
let allAtOnce = await clock.measure { _ = await concurrent() }

print("same answer:", await sequential() == concurrent())
print("sequential waited three times:", oneAfterAnother > .milliseconds(250))
print("concurrent waited once:", allAtOnce < .milliseconds(250))
same answer: true
sequential waited three times: true
concurrent waited once: true

sequential awaits each call before starting the next, so three hundred- millisecond waits take three hundred milliseconds. async let starts the work immediately and the await happens where the value is used, so the three waits overlap and the whole thing takes a hundred.

One await can cover several calls in an expression, which is why await sequential() == concurrent() has a single one. It marks the statement as containing a suspension, not each individual call.

An async function can only be called from another async function, or from a Task, which is the bridge from synchronous code:

Task {
    let answer = await slowSquare(9)
    print(answer)
}

The examples here get away without one because a single file compiled by swiftc is top-level code, and top-level code may await directly. A real program uses @main on a type with a static func main() async, in a file not named main.swift, which is what swift build expects.

12.2 Structured Concurrency

async let handles a fixed number of children known when you write the code. When the number depends on the data, use a task group.

// A task group runs a variable number of children and collects their results.
func measure(_ word: String) async -> (word: String, length: Int) {
    try? await Task.sleep(for: .milliseconds(20))
    return (word, word.count)
}

let words = ["ada", "grace", "alan", "edsger", "barbara"]

var results: [(word: String, length: Int)] = []

await withTaskGroup(of: (word: String, length: Int).self) { group in
    for word in words {
        group.addTask { await measure(word) }
    }
    for await result in group {
        results.append(result)
    }
}

// Results arrive in whatever order they finish, so sort before printing.
for result in results.sorted(by: { $0.word < $1.word }) {
    print(result.word, result.length)
}
print("children:", results.count)
ada 3
alan 4
barbara 7
edsger 6
grace 5
children: 5

Both forms are structured: the children cannot outlive the scope that started them. withTaskGroup does not return until every child has finished, and a function with async let awaits or cancels its children before it returns. That is what makes the lifetimes something you can reason about by reading, rather than something you track with handles and hope.

Cancellation travels down the same tree. Canceling a parent cancels its children, and cancellation is cooperative: a task is asked to stop and must notice, by checking Task.isCancelled or by calling something like Task.checkCancellation() that throws. Work that never checks never stops.

for await walks an AsyncSequence, which is the asynchronous cousin of Chapter 10’s Sequence: a group is one, and so are the lines of a file and the values of a stream.

An unstructured Task { } is the escape hatch, for work that genuinely must outlive its caller. It gives you a handle you can await or cancel, and it is your responsibility rather than the scope’s. Prefer the structured forms.

12.3 Actors

An actor is a reference type that protects its own mutable state. Only one task executes inside an actor at a time, so its properties cannot be read and written simultaneously from two places.

// An actor owns its state. Only one task is inside it at a time.
actor Tally {
    private var counts: [String: Int] = [:]

    func add(_ key: String) {
        counts[key, default: 0] += 1
    }

    func snapshot() -> [String: Int] {
        counts
    }
}

let tally = Tally()

await withTaskGroup(of: Void.self) { group in
    for i in 0..<1000 {
        group.addTask {
            await tally.add(i.isMultiple(of: 2) ? "even" : "odd")
        }
    }
}

let final = await tally.snapshot()
print("even", final["even"] ?? 0)
print("odd ", final["odd"] ?? 0)
print("total", final.values.reduce(0, +))
even 500
odd  500
total 1000

A thousand tasks increment a shared dictionary and the answer is right, with no lock written anywhere. Calls from outside the actor are awaited, because they may have to wait for whoever is inside; calls between an actor’s own methods are not.

@MainActor is a global actor, a single actor that a whole set of declarations belongs to. Marking a type or a function @MainActor means it runs on the main thread, which is what user-interface code requires. In Swift 6 the top level of a program is @MainActor by default, which is why a global var in a script cannot be touched from a detached task without saying more.

The rule that catches people: an actor protects its state, not the objects its state refers to. Handing out a reference to a mutable class from inside an actor hands out the race you were avoiding. Keep actor state in value types and this does not arise.

12.4 Sendable

A value that crosses from one concurrency domain to another must be safe to send, which is what Sendable means. Value types made of Sendable parts are Sendable automatically, as are actors and immutable classes. A mutable class is not, and Swift 6 refuses to let one cross:

data_race.swift:10:13: error: non-Sendable type 'Counter' of let 'counter' cannot exit main actor-isolated context [#NonSendableExitingActor]
 1 | final class Counter {
   |             `- note: class 'Counter' does not conform to the 'Sendable' protocol
 9 |         group.addTask {
10 |             counter.value += 1

[#NonSendableExitingActor]: <https://docs.swift.org/compiler/documentation/diagnostics/getting-non-sendable-values-out-of-actors>

In Swift 5 that program compiles and produces a number slightly under a hundred, different on each run, for reasons that take an afternoon to find. In Swift 6 it does not compile. That is the trade the language is making, and it is the reason strict concurrency was worth a major version.

The fixes, in the order to try them. Make the type a struct, so each task gets its own copy and there is nothing to share. Make it an actor, so the sharing is mediated. Make the class immutable — let properties all the way down — and mark it Sendable. Only when none of those fit, mark it @unchecked Sendable and take responsibility for the locking yourself, which is a promise to the compiler that you have done what it could not verify.

This is where Chapter 7 pays off. A program built out of values has very little that needs to be Sendable in the first place, because there is nothing shared to race over. A program built out of mutable objects has to solve this problem everywhere at once, and that is what porting an old codebase to Swift 6 feels like.

Exercise 12-1. Rewrite taskgroup.swift with async let and find out why you cannot, without changing the shape of the input.

Exercise 12-2. Make measure throw for an empty word, switch to withThrowingTaskGroup, and decide what should happen to the other children when one fails.

Exercise 12-3. Replace the actor in actors.swift with a final class and read the error. Then fix it three ways: a struct, an actor, and @unchecked Sendable with a lock.

Exercise 12-4. Start a Task that loops forever, cancel it after a moment, and make it actually stop.

12.5 What This Book Left Out

Property wrappers and result builders, which is how @State and SwiftUI’s view syntax are built. Macros, which is what #expect is. Unsafe pointers and manual memory, which you need for talking to C. Objective-C interoperation. The whole of SwiftUI, UIKit, and the Apple frameworks, which are a larger subject than the language and are documented by the people who wrote them.

What you have is the language: its type system, its two kinds of type and the reason for choosing between them, the absence of null, the exhaustive switch, protocols and generics, and a concurrency model that checks its own promises. Everything above is written in terms of those, so you now have the vocabulary to read it.

The next thing to write is a program you want. Pick one that reads something and prints something, keep the logic in a module and the world at the edges, and let the compiler argue with you. It is usually right.