← All lessonsLesson 37 of 57 · Part 11: Swift concurrency
Lesson 37

The hive hums: what await really does

Tasks, await, and the main actor: what the words mean

You will buildHive, a meadow with a hive whose queen is the main actor. The hive hums ten times a second while the queen is free, and every errand is measured against the hum: a bee's flight to a flower (a request, awaited), counting the pollen (a long computation, which freezes the hive), the same count marked @concurrent, three bees sent out together and called home with one cancel, a honey jar that is an actor, #isolation, a swarm of ten thousand bees on a handful of threads, and one bee's task looked inside while she waits — the pieces the compiler cut her code into, and her frame, kept on the heap. Each piece of syntax is shown written out by hand — the expansions are the definitions.

TimeAbout two hours

WhyLesson 15 showed what async and await do from the outside, and you have used them, with .task, ever since. This is the lesson for what they are — and most people who use them every day could not say. Interviewers can: "Does await block the main thread?", "When do you need @concurrent?", "What is the difference between a task and a thread?" Behind each is one picture of what the compiler does with your code, and the picture is small enough to draw in one lesson. In this lesson you draw it — by watching a hive hum, and by writing out by hand what each piece of syntax stands for.

Step 1

Meet the hive

Critical viewing

WWDC21: Swift concurrency: Behind the scenes (2021 — the thread model is unchanged)

Apple's own account of the machinery this lesson takes apart, from the engineers who built it: why a waiting task holds no thread, the pool of threads sized to the cores, a function broken up into continuations at its awaits, the async frames kept on the heap, and the hop between actors. Thirty-nine minutes. Watch it with this lesson — steps 9 and 10 measure in the app what it describes.

This lesson's project is a new one, and it begins as a download.

  1. Download Hive-starter.zip, this lesson's starting point. Double-click it in Finder to unzip it, then double-click Hive.xcodeproj to open it in Xcode.

  2. Run it before changing anything. In the toolbar, open the run destination menu and pick an iPhone simulator — iPhone 17, for example. Choose Product ▸ Run (⌘R). Device Hub opens with the app:

    The hive, humming — screenshot
    The hive, hummingScreenshot · iPhone 17, iOS 27.0

The starter is set up as Parcel was at the end of lesson 15: Swift 6, Default Actor Isolation set to nonisolated, and Approachable Concurrency off. So nothing decides for you where code runs — and that is this lesson's subject. Every function you add says where it runs, in its first word, and the console checks what the word said.

Four files. HiveApp.swift is what Xcode wrote. HiveArt.swift draws the meadow, the hive, the queen and the bees — ready-made, nothing to change. Where.swift is whereAmI() from lesson 1, which names the actor a call is on; step 8 takes it apart. And ContentView.swift is where you work. Look at the bottom of it:

ContentView.swift, as the starter has it — the hum is its last modifier
import SwiftUI

struct ContentView: View {
    @State private var hum = 0
    @State private var bees: [Bee] = []
    @State private var report = ""

    var body: some View {
        ZStack {
            HiveArt(hum: hum, bees: bees)
            VStack(spacing: 10) {
                if !report.isEmpty {
                    Text(report)
                        .font(.system(.subheadline, design: .rounded).weight(.semibold))
                        .multilineTextAlignment(.center)
                        .padding(.horizontal, 14)
                        .padding(.vertical, 8)
                        .background(.white.opacity(0.9), in: RoundedRectangle(cornerRadius: 14))
                }
            }
            .frame(maxHeight: .infinity, alignment: .bottom)
            .padding(.bottom, 24)
            .padding(.horizontal, 20)
        }
        .task {
            while !Task.isCancelled {
                try? await Task.sleep(for: .milliseconds(100))
                hum += 1
            }
        }
    }
}

#Preview {
    ContentView()
}

That is the hum. .task starts a task when the view appears — a unit of work with a priority, a flag that says whether it has been cancelled, and an actor to run on — and cancels it when the view goes away. This one wakes ten times a second and adds one to hum. It runs on the main actor, because the view's code does; so the hive hums for exactly as long as the queen — the main actor — is free to run it. Every errand in this lesson is measured against that hum.

Step 2

A bee flies to a flower

The first errand is the one every app has: a request to a server. Make a new Swift file named Bees (File ▸ New ▸ File from Template… (⌘N), Swift File), and make it read:

New fileBees.swift — the whole file
import Foundation

/// The flowers are a web server: Open-Meteo's forecast for the meadow. A request is the wait every app has.
let meadow = URL(string: "https://api.open-meteo.com/v1/forecast?latitude=47.6&longitude=-122.3&current=temperature_2m")!

/// Ada flies to a flower and comes back with what she found. The flight is an `await`.
/// `@MainActor`: she leaves from the queen's actor — on purpose, to see what the wait does to it.
@MainActor func fly() async throws -> Int {
    print("Ada leaves from \(whereAmI())")
    let (data, _) = try await URLSession.shared.data(from: meadow)
    print("Ada is back on \(whereAmI())")
    return data.count
}

fly() is async, and the request is an await. It says @MainActor: in this project a function belongs to no actor unless it says so, and Ada sets out from the queen's actor on purpose — to see what the wait does to it. Now give the view a way to send Ada, and a way to measure every errand. Replace the ContentView struct with:

In ContentView.swift, replace the ContentView struct with this
struct ContentView: View {    @State private var hum = 0    @State private var bees: [Bee] = []    @State private var report = ""    var body: some View {        ZStack {            HiveArt(hum: hum, bees: bees)            VStack(spacing: 10) {                if !report.isEmpty {                    Text(report)                        .font(.system(.subheadline, design: .rounded).weight(.semibold))                        .multilineTextAlignment(.center)                        .padding(.horizontal, 14)                        .padding(.vertical, 8)                        .background(.white.opacity(0.9), in: RoundedRectangle(cornerRadius: 14))                }                Button("Send Ada to a flower") {                    Task {                        bees = [Bee(id: "Ada", place: "flying to the 🌻")]                        await errand("Ada's flight") { "\(try await fly()) bytes of nectar" }                        bees = [Bee(id: "Ada", place: "home")]                    }                }                .buttonStyle(.borderedProminent)                .tint(Color(red: 0.85, green: 0.55, blue: 0.1))            }            .frame(maxHeight: .infinity, alignment: .bottom)            .padding(.bottom, 24)            .padding(.horizontal, 20)        }        .task {            while !Task.isCancelled {                try? await Task.sleep(for: .milliseconds(100))                hum += 1            }        }    }    /// One errand: what it brought back, how long it took, and how often the hive hummed meanwhile.    func errand(_ name: String, _ work: () async throws -> String) async {        let before = hum, start = Date()        do {            let result = try await work()            let seconds = Date().timeIntervalSince(start).formatted(.number.precision(.fractionLength(1)))            report = "\(name): \(result), \(seconds) s. The hive hummed \(hum - before) times meanwhile."        } catch {            report = "\(name) failed: \(error.localizedDescription)"        }        print(report)    }}
struct ContentView: View {
    @State private var hum = 0
    @State private var bees: [Bee] = []
    @State private var report = ""

    var body: some View {
        ZStack {
            HiveArt(hum: hum, bees: bees)
            VStack(spacing: 10) {
                if !report.isEmpty {
                    Text(report)
                        .font(.system(.subheadline, design: .rounded).weight(.semibold))
                        .multilineTextAlignment(.center)
                        .padding(.horizontal, 14)
                        .padding(.vertical, 8)
                        .background(.white.opacity(0.9), in: RoundedRectangle(cornerRadius: 14))
                }
                Button("Send Ada to a flower") {
                    Task {
                        bees = [Bee(id: "Ada", place: "flying to the 🌻")]
                        await errand("Ada's flight") { "\(try await fly()) bytes of nectar" }
                        bees = [Bee(id: "Ada", place: "home")]
                    }
                }
                .buttonStyle(.borderedProminent)
                .tint(Color(red: 0.85, green: 0.55, blue: 0.1))
            }
            .frame(maxHeight: .infinity, alignment: .bottom)
            .padding(.bottom, 24)
            .padding(.horizontal, 20)
        }
        .task {
            while !Task.isCancelled {
                try? await Task.sleep(for: .milliseconds(100))
                hum += 1
            }
        }
    }

    /// One errand: what it brought back, how long it took, and how often the hive hummed meanwhile.
    func errand(_ name: String, _ work: () async throws -> String) async {
        let before = hum, start = Date()
        do {
            let result = try await work()
            let seconds = Date().timeIntervalSince(start).formatted(.number.precision(.fractionLength(1)))
            report = "\(name): \(result), \(seconds) s. The hive hummed \(hum - before) times meanwhile."
        } catch {
            report = "\(name) failed: \(error.localizedDescription)"
        }
        print(report)
    }
}

errand notes the hum before the work and after it, and reports the difference. Run the app and send Ada:

Ada is back, and the hive hummed the whole time — screenshot
Ada is back, and the hive hummed the whole timeScreenshot · iPhone 17, iOS 27.0
Console
Ada leaves from MainActor
Ada is back on MainActor
Ada's flight: 323 bytes of nectar, 0.5 s. The hive hummed 4 times meanwhile.

The request took a fraction of a second, and the hive hummed right through it — once every tenth of a second, as if nothing were happening. fly() left from the main actor, waited, and came back to the main actor — and the main actor was free in between. That is the answer to the question most people cannot answer: await does not block the thread. It suspends the task.

Here is what that means, in Swift. The compiler cuts fly() in two at the await. The first half runs up to the request and returns — the thread is free. When the data arrives, the second half runs, with the locals it needs handed along, back on the actor the first half was on. You could write it yourself:

Bees.swift — fly(), written out by hand, for comparison only
import Foundation

// fly(), as the compiler builds it: two ordinary functions, and the request's completion handler between them.
// You never write this. It is what `await` means.
@MainActor func flyFirstHalf(then: @escaping @Sendable (Int) -> Void) {
    // everything before the await
    print("Ada leaves from \(whereAmI())")
    // the wait. The closure runs when the data has arrived, on some other thread —
    // so it hops back to the actor the first half was on, and runs the second half there
    URLSession.shared.dataTask(with: meadow) { data, _, _ in
        Task { @MainActor in
            flySecondHalf(data ?? Data(), then: then)
        }
    }.resume()
    // and the first half returns here: the thread is free
}

@MainActor func flySecondHalf(_ data: Data, then: (Int) -> Void) {
    // everything after the await
    print("Ada is back on \(whereAmI())")
    then(data.count)
}

A 2016 iOS developer wrote exactly this, by hand, for every request: a completion handler, the "second half" of the function inside it, and a hop back to the main queue. async/await is the compiler writing it for you. Everything that follows from it follows from this picture: the locals of an async function live in a frame the compiler keeps off the thread, so that the function can stop and start; a thread is never kept waiting at an await; and the second half runs on the actor the first half was on — here, the main actor — so the code after an await can touch the view's state.

The bridge from GCD

Think of the main actor as DispatchQueue.main. A completion handler that hops back to the main queue is what the second half does. The difference is who writes it, and one more thing: a GCD block holds its queue to the end, while an async function hands its actor back at every await. That difference is step 7.

Where this comes from: SE-0296: Async/await · WWDC21: Swift concurrency: Behind the scenes (2021 — the thread model is unchanged)

Step 3

Counting the pollen freezes the hive

The second errand has no request in it: counting every grain of pollen in the meadow, a long computation. Add to Bees.swift:

Add to Bees.swift
/// Counting every grain of pollen in the meadow: a long computation, with nothing to wait for.
/// `@MainActor`, like `fly()`: on the queen's actor.
@MainActor func countPollen() async -> Int {
    var grains = 0
    for i in 0..<60_000_000 where sin(Double(i)) > 0.999 {
        grains += 1
    }
    print("counted on \(whereAmI())")
    return grains
}

And a button for it. Replace the ContentView struct — the change is a second button beside the first, in an HStack:

In ContentView.swift, replace the ContentView struct with this
struct ContentView: View {    @State private var hum = 0    @State private var bees: [Bee] = []    @State private var report = ""    var body: some View {        ZStack {            HiveArt(hum: hum, bees: bees)            VStack(spacing: 10) {                if !report.isEmpty {                    Text(report)                        .font(.system(.subheadline, design: .rounded).weight(.semibold))                        .multilineTextAlignment(.center)                        .padding(.horizontal, 14)                        .padding(.vertical, 8)                        .background(.white.opacity(0.9), in: RoundedRectangle(cornerRadius: 14))                }                Button("Send Ada to a flower") {                    Task {                        bees = [Bee(id: "Ada", place: "flying to the 🌻")]                        await errand("Ada's flight") { "\(try await fly()) bytes of nectar" }                        bees = [Bee(id: "Ada", place: "home")]                HStack {                    Button("Send Ada to a flower") {                        Task {                            bees = [Bee(id: "Ada", place: "flying to the 🌻")]                            await errand("Ada's flight") { "\(try await fly()) bytes of nectar" }                            bees = [Bee(id: "Ada", place: "home")]                        }                    }                    Button("Count the pollen") {                        Task {                            await errand("Counting the pollen") { "\(await countPollen()) grains" }                        }                    }                }                .buttonStyle(.borderedProminent)                .tint(Color(red: 0.85, green: 0.55, blue: 0.1))            }            .frame(maxHeight: .infinity, alignment: .bottom)            .padding(.bottom, 24)            .padding(.horizontal, 20)        }        .task {            while !Task.isCancelled {                try? await Task.sleep(for: .milliseconds(100))                hum += 1            }        }    }    /// One errand: what it brought back, how long it took, and how often the hive hummed meanwhile.    func errand(_ name: String, _ work: () async throws -> String) async {        let before = hum, start = Date()        do {            let result = try await work()            let seconds = Date().timeIntervalSince(start).formatted(.number.precision(.fractionLength(1)))            report = "\(name): \(result), \(seconds) s. The hive hummed \(hum - before) times meanwhile."        } catch {            report = "\(name) failed: \(error.localizedDescription)"        }        print(report)    }}
struct ContentView: View {
    @State private var hum = 0
    @State private var bees: [Bee] = []
    @State private var report = ""

    var body: some View {
        ZStack {
            HiveArt(hum: hum, bees: bees)
            VStack(spacing: 10) {
                if !report.isEmpty {
                    Text(report)
                        .font(.system(.subheadline, design: .rounded).weight(.semibold))
                        .multilineTextAlignment(.center)
                        .padding(.horizontal, 14)
                        .padding(.vertical, 8)
                        .background(.white.opacity(0.9), in: RoundedRectangle(cornerRadius: 14))
                }
                HStack {
                    Button("Send Ada to a flower") {
                        Task {
                            bees = [Bee(id: "Ada", place: "flying to the 🌻")]
                            await errand("Ada's flight") { "\(try await fly()) bytes of nectar" }
                            bees = [Bee(id: "Ada", place: "home")]
                        }
                    }
                    Button("Count the pollen") {
                        Task {
                            await errand("Counting the pollen") { "\(await countPollen()) grains" }
                        }
                    }
                }
                .buttonStyle(.borderedProminent)
                .tint(Color(red: 0.85, green: 0.55, blue: 0.1))
            }
            .frame(maxHeight: .infinity, alignment: .bottom)
            .padding(.bottom, 24)
            .padding(.horizontal, 20)
        }
        .task {
            while !Task.isCancelled {
                try? await Task.sleep(for: .milliseconds(100))
                hum += 1
            }
        }
    }

    /// One errand: what it brought back, how long it took, and how often the hive hummed meanwhile.
    func errand(_ name: String, _ work: () async throws -> String) async {
        let before = hum, start = Date()
        do {
            let result = try await work()
            let seconds = Date().timeIntervalSince(start).formatted(.number.precision(.fractionLength(1)))
            report = "\(name): \(result), \(seconds) s. The hive hummed \(hum - before) times meanwhile."
        } catch {
            report = "\(name) failed: \(error.localizedDescription)"
        }
        print(report)
    }
}

Run it, and tap Count the pollen. Watch the hum.

The count is done. For several seconds the hive did not hum once — screenshot
The count is done. For several seconds the hive did not hum onceScreenshot · iPhone 17, iOS 27.0

Two pictures of the screen during the count, two seconds apart:

A second into the count — screenshot
A second into the countScreenshot · iPhone 17, iOS 27.0
Two seconds later: the same picture, to the pixel. The screen did not draw — screenshot
Two seconds later: the same picture, to the pixel. The screen did not drawScreenshot · iPhone 17, iOS 27.0
Console
counted on MainActor
Counting the pollen: 854189 grains, 4.5 s. The hive hummed 0 times meanwhile.

countPollen() is async, and the button awaits it, exactly like fly(). It froze the hive anyway, because nothing in it ever suspends: there is no request to wait for, only arithmetic, and arithmetic runs on whatever actor the function is on — the main actor, as its first word says. await is not a request to run elsewhere. It is a place where the task may suspend, and a function with nothing to wait for never does.

So the rule, which lesson 42 states and this is the reason for: waiting on the main actor is free; computing on it is not. A request, a file read, a Task.sleep — the main actor is free while they wait. A parse, an image resize, a long loop — the main actor is busy until they end. The GCD habit of pushing every slow call to a background queue bought nothing for the first kind, and still buys nothing. For the second kind, there is one word.

Step 4

@concurrent: the same count, on the pool

Replace countPollen in Bees.swift with this version. The body is identical; the only change is the first word:

In Bees.swift, replace countPollen with this
/// Counting every grain of pollen in the meadow: a long computation, with nothing to wait for./// `@MainActor`, like `fly()`: on the queen's actor.@MainActor func countPollen() async -> Int {/// Counting every grain of pollen in the meadow: a long computation, with nothing to wait for —/// now on the shared thread pool, so that the queen is free while it runs.@concurrent func countPollen() async -> Int {    var grains = 0    for i in 0..<60_000_000 where sin(Double(i)) > 0.999 {        grains += 1    }    print("counted on \(whereAmI())")    return grains}
/// Counting every grain of pollen in the meadow: a long computation, with nothing to wait for —
/// now on the shared thread pool, so that the queen is free while it runs.
@concurrent func countPollen() async -> Int {
    var grains = 0
    for i in 0..<60_000_000 where sin(Double(i)) > 0.999 {
        grains += 1
    }
    print("counted on \(whereAmI())")
    return grains
}

Run it and count again:

The same count, and the hive hummed through it — screenshot
The same count, and the hive hummed through itScreenshot · iPhone 17, iOS 27.0
A second into the count — screenshot
A second into the countScreenshot · iPhone 17, iOS 27.0
Two seconds later: the hum went on — screenshot
Two seconds later: the hum went onScreenshot · iPhone 17, iOS 27.0
Console
counted on no actor
Counting the pollen: 854189 grains, 4.0 s. The hive hummed 38 times meanwhile.

The same grains, in about the same time, and the hive hummed some forty times meanwhile. The count ran on no actor: @concurrent says that this async function runs on the shared thread pool, whoever calls it, and that its caller's actor is free until it returns. The button's await is unchanged. What changed is where the task went between that await and the return.

Written out, @concurrent is the Swift people wrote before it existed — hand the body to the pool and wait for it:

Bees.swift — @concurrent, written out by hand, for comparison only
import Foundation

// @concurrent, written out as the Swift you could write before it existed: hand the body to the shared pool,
// and wait for it, where you are. The queen is free while you wait.
nonisolated(nonsending) func countPollenByHand() async -> Int {
    // a new task, on the shared pool, on no actor — and .value: suspend here until it is done
    await Task.detached {
        var grains = 0
        for i in 0..<60_000_000 where sin(Double(i)) > 0.999 {
            grains += 1
        }
        print("counted on \(whereAmI())")
        return grains
    }.value
}

With one difference, and it is the reason the word exists. Task.detached makes a new task — its own priority, its own cancellation, none of the caller's. @concurrent moves the same task to the pool and back: cancel the caller, and the @concurrent function sees it; a detached task does not. Step 5 has the cancel, and one of this lesson's cards has the proof.

The trap

A @concurrent function that calls main-actor code — a function marked @MainActor, a view's property — hops back to the main actor to do it, and the freeze is back. The work has to be on no actor all the way down: in this project, anything that does not say @MainActor; in a new Xcode project, where everything is on the main actor by default, only what says nonisolated. One of this lesson's edge cases shows the hop, and a flash card shows it in a new project.

Where this comes from: SE-0461: nonisolated(nonsending) and @concurrent

Step 5

Three bees, and what a task is

Ada flew alone. Send three bees at once, and bring them home with one word. Add to Bees.swift:

Add to Bees.swift
/// One bee's errand: so long at a flower, then back — unless she is called home first.
/// `nonisolated(nonsending)`: she only waits, so she stays on whatever actor sends her.
nonisolated(nonsending) func visit(_ bee: String, _ flower: String, for ms: Int) async -> String {
    do {
        try await Task.sleep(for: .milliseconds(ms))
        return "\(bee) back from the \(flower)"
    } catch {
        return "\(bee) called home from the \(flower)"
    }
}

/// Three bees at once: three child tasks. They end with this function, and are cancelled with it.
/// `nonisolated(nonsending)`, like `visit`: it only waits for them.
nonisolated(nonsending) func sendThree() async -> [String] {
    async let ada = visit("Ada", "🌻", for: 1000)
    async let bea = visit("Bea", "🌸", for: 3500)
    async let cia = visit("Cia", "🌷", for: 6000)
    return await [ada, bea, cia]
}

async let starts each visit at once, as a child task of sendThree's task, and the await at the end waits for all three. Both functions say nonisolated(nonsending), the words lesson 15 gave a function that only waits: each runs on whatever actor calls it. Give the view two more buttons. Replace the ContentView struct:

In ContentView.swift, replace the ContentView struct with this
struct ContentView: View {    @State private var hum = 0    @State private var bees: [Bee] = []    @State private var report = ""    @State private var trip: Task<Void, Never>?    var body: some View {        ZStack {            HiveArt(hum: hum, bees: bees)            VStack(spacing: 10) {                if !report.isEmpty {                    Text(report)                        .font(.system(.subheadline, design: .rounded).weight(.semibold))                        .multilineTextAlignment(.center)                        .padding(.horizontal, 14)                        .padding(.vertical, 8)                        .background(.white.opacity(0.9), in: RoundedRectangle(cornerRadius: 14))                }                HStack {                    Button("Send Ada to a flower") {                        Task {                            bees = [Bee(id: "Ada", place: "flying to the 🌻")]                            await errand("Ada's flight") { "\(try await fly()) bytes of nectar" }                            bees = [Bee(id: "Ada", place: "home")]                        }                    }                    Button("Count the pollen") {                        Task {                            await errand("Counting the pollen") { "\(await countPollen()) grains" }                        }                    }                }                .buttonStyle(.borderedProminent)                .tint(Color(red: 0.85, green: 0.55, blue: 0.1))                HStack {                    Button("Send three bees") {                        trip = Task {                            bees = [Bee(id: "Ada", place: "at the 🌻"), Bee(id: "Bea", place: "at the 🌸"), Bee(id: "Cia", place: "at the 🌷")]                            await errand("Three bees") { await sendThree().joined(separator: "; ") }                            bees = []                        }                    }                    Button("Call them home") {                        trip?.cancel()                    }                }            }            .buttonStyle(.borderedProminent)            .tint(Color(red: 0.85, green: 0.55, blue: 0.1))            .frame(maxHeight: .infinity, alignment: .bottom)            .padding(.bottom, 24)            .padding(.horizontal, 20)        }        .task {            while !Task.isCancelled {                try? await Task.sleep(for: .milliseconds(100))                hum += 1            }        }    }    /// One errand: what it brought back, how long it took, and how often the hive hummed meanwhile.    func errand(_ name: String, _ work: () async throws -> String) async {        let before = hum, start = Date()        do {            let result = try await work()            let seconds = Date().timeIntervalSince(start).formatted(.number.precision(.fractionLength(1)))            report = "\(name): \(result), \(seconds) s. The hive hummed \(hum - before) times meanwhile."        } catch {            report = "\(name) failed: \(error.localizedDescription)"        }        print(report)    }}
struct ContentView: View {
    @State private var hum = 0
    @State private var bees: [Bee] = []
    @State private var report = ""
    @State private var trip: Task<Void, Never>?

    var body: some View {
        ZStack {
            HiveArt(hum: hum, bees: bees)
            VStack(spacing: 10) {
                if !report.isEmpty {
                    Text(report)
                        .font(.system(.subheadline, design: .rounded).weight(.semibold))
                        .multilineTextAlignment(.center)
                        .padding(.horizontal, 14)
                        .padding(.vertical, 8)
                        .background(.white.opacity(0.9), in: RoundedRectangle(cornerRadius: 14))
                }
                HStack {
                    Button("Send Ada to a flower") {
                        Task {
                            bees = [Bee(id: "Ada", place: "flying to the 🌻")]
                            await errand("Ada's flight") { "\(try await fly()) bytes of nectar" }
                            bees = [Bee(id: "Ada", place: "home")]
                        }
                    }
                    Button("Count the pollen") {
                        Task {
                            await errand("Counting the pollen") { "\(await countPollen()) grains" }
                        }
                    }
                }
                HStack {
                    Button("Send three bees") {
                        trip = Task {
                            bees = [Bee(id: "Ada", place: "at the 🌻"), Bee(id: "Bea", place: "at the 🌸"), Bee(id: "Cia", place: "at the 🌷")]
                            await errand("Three bees") { await sendThree().joined(separator: "; ") }
                            bees = []
                        }
                    }
                    Button("Call them home") {
                        trip?.cancel()
                    }
                }
            }
            .buttonStyle(.borderedProminent)
            .tint(Color(red: 0.85, green: 0.55, blue: 0.1))
            .frame(maxHeight: .infinity, alignment: .bottom)
            .padding(.bottom, 24)
            .padding(.horizontal, 20)
        }
        .task {
            while !Task.isCancelled {
                try? await Task.sleep(for: .milliseconds(100))
                hum += 1
            }
        }
    }

    /// One errand: what it brought back, how long it took, and how often the hive hummed meanwhile.
    func errand(_ name: String, _ work: () async throws -> String) async {
        let before = hum, start = Date()
        do {
            let result = try await work()
            let seconds = Date().timeIntervalSince(start).formatted(.number.precision(.fractionLength(1)))
            report = "\(name): \(result), \(seconds) s. The hive hummed \(hum - before) times meanwhile."
        } catch {
            report = "\(name) failed: \(error.localizedDescription)"
        }
        print(report)
    }
}

The trip is kept in trip, a Task of its own, so that Call them home can cancel it. Run the app and send the three:

Three bees out at once: six seconds, not ten and a half — screenshot
Three bees out at once: six seconds, not ten and a halfScreenshot · iPhone 17, iOS 27.0
All three in the meadow — screenshot
All three in the meadowScreenshot · iPhone 17, iOS 27.0
Console
Three bees: Ada back from the 🌻; Bea back from the 🌸; Cia back from the 🌷, 6.1 s. The hive hummed 59 times meanwhile.

The slowest bee takes six seconds, and so does the whole trip — not the ten and a half of three visits one after another: the three ran at the same time. Now send them again, and after a second or two tap Call them home:

Called home: Ada was already back; Bea and Cia turned round — screenshot
Called home: Ada was already back; Bea and Cia turned roundScreenshot · iPhone 17, iOS 27.0
Console
Three bees: Ada back from the 🌻; Bea called home from the 🌸; Cia called home from the 🌷, 2.6 s. The hive hummed 24 times meanwhile.

One cancel() on the trip reached every child. That is what structured means: async let, task groups and .task make tasks whose life belongs to the code that made them — they cannot outlive it, and cancelling the parent cancels them. Task { } makes an unstructured task that nothing owns: it is not cancelled with anything, and it runs until it is done or someone who kept it calls cancel(). The trip itself is one — which is why the view keeps it in trip.

Cancellation is only a flag. Task.sleep checks it and throws; a loop of your own has to look, with Task.isCancelled or try Task.checkCancellation(). Nothing is stopped from outside.

And async let, written out:

Bees.swift — async let, written out by hand, for comparison only
// async let, written out: a task group with three children. Each child is a task of its own, started at once;
// the group waits for all of them, and cancelling the task that runs sendThreeByHand cancels every child.
nonisolated(nonsending) func sendThreeByHand() async -> [String] {
    await withTaskGroup(of: (Int, String).self) { group in
        group.addTask { (0, await visit("Ada", "🌻", for: 1000)) }
        group.addTask { (1, await visit("Bea", "🌸", for: 3500)) }
        group.addTask { (2, await visit("Cia", "🌷", for: 6000)) }
        var results = ["", "", ""]
        for await (i, result) in group {
            results[i] = result
        }
        return results
    }
}

Three child tasks in a group, each started at once, all awaited at the end, all cancelled with the group's own task. async let is that group, for the case of a fixed, named few.

A task is not a thread

The three bees are three tasks, and the hive has one main thread and a small pool. A task is a piece of work that can stop at an await and start again later, on whatever thread is free; a thread is what runs it in between. Hundreds of tasks share a handful of threads, and none of them holds a thread while it waits — step 9 counts ten thousand, and step 10 opens one up. In GCD the nearest thing to a task was a block on a queue — and a block holds its thread to the end.

Where this comes from: SE-0304: Structured concurrency · Task

Step 6

The honey jar is an actor

Every bee pours into the same jar, from whatever thread her task is on — the situation a lock used to be for. Make a new Swift file named Jar, and make it read:

New fileJar.swift — the whole file
import Foundation

/// The honey jar. Every bee pours into the same one, so it is an actor: one bee in the jar at a time.
actor Jar {
    private(set) var drops = 0

    func pour() {
        drops += 1
        // two drops, one after the other: a read, a change and a write, twice
        drops += 1
    }

    func empty() {
        drops = 0
    }

    func whereIsTheJar() -> String {
        "the jar is on \(whereAmI())"
    }
}

actor instead of class: the jar's state can be touched by one task at a time, and every call into it from outside is an await. Give the view a jar and a button that sends 500 bees to it, each adding two drops. Replace the ContentView struct:

In ContentView.swift, replace the ContentView struct with this
struct ContentView: View {    @State private var hum = 0    @State private var bees: [Bee] = []    @State private var report = ""    @State private var trip: Task<Void, Never>?    @State private var jar = Jar()    var body: some View {        ZStack {            HiveArt(hum: hum, bees: bees)            VStack(spacing: 10) {                if !report.isEmpty {                    Text(report)                        .font(.system(.subheadline, design: .rounded).weight(.semibold))                        .multilineTextAlignment(.center)                        .padding(.horizontal, 14)                        .padding(.vertical, 8)                        .background(.white.opacity(0.9), in: RoundedRectangle(cornerRadius: 14))                }                HStack {                    Button("Send Ada to a flower") {                        Task {                            bees = [Bee(id: "Ada", place: "flying to the 🌻")]                            await errand("Ada's flight") { "\(try await fly()) bytes of nectar" }                            bees = [Bee(id: "Ada", place: "home")]                        }                    }                    Button("Count the pollen") {                        Task {                            await errand("Counting the pollen") { "\(await countPollen()) grains" }                        }                    }                }                HStack {                    Button("Send three bees") {                        trip = Task {                            bees = [Bee(id: "Ada", place: "at the 🌻"), Bee(id: "Bea", place: "at the 🌸"), Bee(id: "Cia", place: "at the 🌷")]                            await errand("Three bees") { await sendThree().joined(separator: "; ") }                            bees = []                        }                    }                    Button("Call them home") {                        trip?.cancel()                    }                }                HStack {                    Button("500 bees pour") {                        Task {                            await jar.empty()                            await errand("500 bees, two drops each") {                                await withTaskGroup(of: Void.self) { group in                                    for _ in 0..<500 {                                        group.addTask { await jar.pour() }                                    }                                }                                print(await jar.whereIsTheJar())                                return "\(await jar.drops) drops in the jar"                            }                        }                    }                }            }            .buttonStyle(.borderedProminent)            .tint(Color(red: 0.85, green: 0.55, blue: 0.1))            .frame(maxHeight: .infinity, alignment: .bottom)            .padding(.bottom, 24)            .padding(.horizontal, 20)        }        .task {            while !Task.isCancelled {                try? await Task.sleep(for: .milliseconds(100))                hum += 1            }        }    }    /// One errand: what it brought back, how long it took, and how often the hive hummed meanwhile.    func errand(_ name: String, _ work: () async throws -> String) async {        let before = hum, start = Date()        do {            let result = try await work()            let seconds = Date().timeIntervalSince(start).formatted(.number.precision(.fractionLength(1)))            report = "\(name): \(result), \(seconds) s. The hive hummed \(hum - before) times meanwhile."        } catch {            report = "\(name) failed: \(error.localizedDescription)"        }        print(report)    }}
struct ContentView: View {
    @State private var hum = 0
    @State private var bees: [Bee] = []
    @State private var report = ""
    @State private var trip: Task<Void, Never>?
    @State private var jar = Jar()

    var body: some View {
        ZStack {
            HiveArt(hum: hum, bees: bees)
            VStack(spacing: 10) {
                if !report.isEmpty {
                    Text(report)
                        .font(.system(.subheadline, design: .rounded).weight(.semibold))
                        .multilineTextAlignment(.center)
                        .padding(.horizontal, 14)
                        .padding(.vertical, 8)
                        .background(.white.opacity(0.9), in: RoundedRectangle(cornerRadius: 14))
                }
                HStack {
                    Button("Send Ada to a flower") {
                        Task {
                            bees = [Bee(id: "Ada", place: "flying to the 🌻")]
                            await errand("Ada's flight") { "\(try await fly()) bytes of nectar" }
                            bees = [Bee(id: "Ada", place: "home")]
                        }
                    }
                    Button("Count the pollen") {
                        Task {
                            await errand("Counting the pollen") { "\(await countPollen()) grains" }
                        }
                    }
                }
                HStack {
                    Button("Send three bees") {
                        trip = Task {
                            bees = [Bee(id: "Ada", place: "at the 🌻"), Bee(id: "Bea", place: "at the 🌸"), Bee(id: "Cia", place: "at the 🌷")]
                            await errand("Three bees") { await sendThree().joined(separator: "; ") }
                            bees = []
                        }
                    }
                    Button("Call them home") {
                        trip?.cancel()
                    }
                }
                HStack {
                    Button("500 bees pour") {
                        Task {
                            await jar.empty()
                            await errand("500 bees, two drops each") {
                                await withTaskGroup(of: Void.self) { group in
                                    for _ in 0..<500 {
                                        group.addTask { await jar.pour() }
                                    }
                                }
                                print(await jar.whereIsTheJar())
                                return "\(await jar.drops) drops in the jar"
                            }
                        }
                    }
                }
            }
            .buttonStyle(.borderedProminent)
            .tint(Color(red: 0.85, green: 0.55, blue: 0.1))
            .frame(maxHeight: .infinity, alignment: .bottom)
            .padding(.bottom, 24)
            .padding(.horizontal, 20)
        }
        .task {
            while !Task.isCancelled {
                try? await Task.sleep(for: .milliseconds(100))
                hum += 1
            }
        }
    }

    /// One errand: what it brought back, how long it took, and how often the hive hummed meanwhile.
    func errand(_ name: String, _ work: () async throws -> String) async {
        let before = hum, start = Date()
        do {
            let result = try await work()
            let seconds = Date().timeIntervalSince(start).formatted(.number.precision(.fractionLength(1)))
            report = "\(name): \(result), \(seconds) s. The hive hummed \(hum - before) times meanwhile."
        } catch {
            report = "\(name) failed: \(error.localizedDescription)"
        }
        print(report)
    }
}

Run it and tap 500 bees pour:

500 bees, two drops each: exactly 1000 — screenshot
500 bees, two drops each: exactly 1000Screenshot · iPhone 17, iOS 27.0
Console
the jar is on Jar
500 bees, two drops each: 1000 drops in the jar, 0.0 s. The hive hummed 0 times meanwhile.

Exactly 1000, every time, with no lock in sight. Inside pour() the jar reports that it is on Jar: its own actor. await jar.pour() from outside means the same two hops as @concurrent — to the jar's executor, where pour runs with no other bee in the jar, and back to the caller's actor — and the jar runs the 500 pours one after another. In GCD terms the jar is a private serial queue, with the compiler refusing to let you forget to use it.

Step 7

Reentrancy: the jar is free at every await

Now a slow pour. Add to the bottom of Jar.swift:

Add to Jar.swift
extension Jar {
    /// A slow pour: look at the jar, go and get the honey, come back and put one drop on top of what you saw.
    func pourSlowly() async {
        let seen = drops
        try? await Task.sleep(for: .milliseconds(10))
        drops = seen + 1
    }
}

A bee looks at the jar, goes to get the honey — an await — and puts one drop on top of what she saw. Give it a button, beside the first: add a second button to the jar's HStack:

In ContentView.swift, replace the ContentView struct with this
struct ContentView: View {    @State private var hum = 0    @State private var bees: [Bee] = []    @State private var report = ""    @State private var trip: Task<Void, Never>?    @State private var jar = Jar()    var body: some View {        ZStack {            HiveArt(hum: hum, bees: bees)            VStack(spacing: 10) {                if !report.isEmpty {                    Text(report)                        .font(.system(.subheadline, design: .rounded).weight(.semibold))                        .multilineTextAlignment(.center)                        .padding(.horizontal, 14)                        .padding(.vertical, 8)                        .background(.white.opacity(0.9), in: RoundedRectangle(cornerRadius: 14))                }                HStack {                    Button("Send Ada to a flower") {                        Task {                            bees = [Bee(id: "Ada", place: "flying to the 🌻")]                            await errand("Ada's flight") { "\(try await fly()) bytes of nectar" }                            bees = [Bee(id: "Ada", place: "home")]                        }                    }                    Button("Count the pollen") {                        Task {                            await errand("Counting the pollen") { "\(await countPollen()) grains" }                        }                    }                }                HStack {                    Button("Send three bees") {                        trip = Task {                            bees = [Bee(id: "Ada", place: "at the 🌻"), Bee(id: "Bea", place: "at the 🌸"), Bee(id: "Cia", place: "at the 🌷")]                            await errand("Three bees") { await sendThree().joined(separator: "; ") }                            bees = []                        }                    }                    Button("Call them home") {                        trip?.cancel()                    }                }                HStack {                    Button("500 bees pour") {                        Task {                            await jar.empty()                            await errand("500 bees, two drops each") {                                await withTaskGroup(of: Void.self) { group in                                    for _ in 0..<500 {                                        group.addTask { await jar.pour() }                                    }                                }                                print(await jar.whereIsTheJar())                                return "\(await jar.drops) drops in the jar"                            }                        }                    }                    Button("100 pour slowly") {                        Task {                            await jar.empty()                            await errand("100 bees, one slow drop each") {                                await withTaskGroup(of: Void.self) { group in                                    for _ in 0..<100 {                                        group.addTask { await jar.pourSlowly() }                                    }                                }                                return "\(await jar.drops) drops in the jar"                            }                        }                    }                }            }            .buttonStyle(.borderedProminent)            .tint(Color(red: 0.85, green: 0.55, blue: 0.1))            .frame(maxHeight: .infinity, alignment: .bottom)            .padding(.bottom, 24)            .padding(.horizontal, 20)        }        .task {            while !Task.isCancelled {                try? await Task.sleep(for: .milliseconds(100))                hum += 1            }        }    }    /// One errand: what it brought back, how long it took, and how often the hive hummed meanwhile.    func errand(_ name: String, _ work: () async throws -> String) async {        let before = hum, start = Date()        do {            let result = try await work()            let seconds = Date().timeIntervalSince(start).formatted(.number.precision(.fractionLength(1)))            report = "\(name): \(result), \(seconds) s. The hive hummed \(hum - before) times meanwhile."        } catch {            report = "\(name) failed: \(error.localizedDescription)"        }        print(report)    }}
struct ContentView: View {
    @State private var hum = 0
    @State private var bees: [Bee] = []
    @State private var report = ""
    @State private var trip: Task<Void, Never>?
    @State private var jar = Jar()

    var body: some View {
        ZStack {
            HiveArt(hum: hum, bees: bees)
            VStack(spacing: 10) {
                if !report.isEmpty {
                    Text(report)
                        .font(.system(.subheadline, design: .rounded).weight(.semibold))
                        .multilineTextAlignment(.center)
                        .padding(.horizontal, 14)
                        .padding(.vertical, 8)
                        .background(.white.opacity(0.9), in: RoundedRectangle(cornerRadius: 14))
                }
                HStack {
                    Button("Send Ada to a flower") {
                        Task {
                            bees = [Bee(id: "Ada", place: "flying to the 🌻")]
                            await errand("Ada's flight") { "\(try await fly()) bytes of nectar" }
                            bees = [Bee(id: "Ada", place: "home")]
                        }
                    }
                    Button("Count the pollen") {
                        Task {
                            await errand("Counting the pollen") { "\(await countPollen()) grains" }
                        }
                    }
                }
                HStack {
                    Button("Send three bees") {
                        trip = Task {
                            bees = [Bee(id: "Ada", place: "at the 🌻"), Bee(id: "Bea", place: "at the 🌸"), Bee(id: "Cia", place: "at the 🌷")]
                            await errand("Three bees") { await sendThree().joined(separator: "; ") }
                            bees = []
                        }
                    }
                    Button("Call them home") {
                        trip?.cancel()
                    }
                }
                HStack {
                    Button("500 bees pour") {
                        Task {
                            await jar.empty()
                            await errand("500 bees, two drops each") {
                                await withTaskGroup(of: Void.self) { group in
                                    for _ in 0..<500 {
                                        group.addTask { await jar.pour() }
                                    }
                                }
                                print(await jar.whereIsTheJar())
                                return "\(await jar.drops) drops in the jar"
                            }
                        }
                    }
                    Button("100 pour slowly") {
                        Task {
                            await jar.empty()
                            await errand("100 bees, one slow drop each") {
                                await withTaskGroup(of: Void.self) { group in
                                    for _ in 0..<100 {
                                        group.addTask { await jar.pourSlowly() }
                                    }
                                }
                                return "\(await jar.drops) drops in the jar"
                            }
                        }
                    }
                }
            }
            .buttonStyle(.borderedProminent)
            .tint(Color(red: 0.85, green: 0.55, blue: 0.1))
            .frame(maxHeight: .infinity, alignment: .bottom)
            .padding(.bottom, 24)
            .padding(.horizontal, 20)
        }
        .task {
            while !Task.isCancelled {
                try? await Task.sleep(for: .milliseconds(100))
                hum += 1
            }
        }
    }

    /// One errand: what it brought back, how long it took, and how often the hive hummed meanwhile.
    func errand(_ name: String, _ work: () async throws -> String) async {
        let before = hum, start = Date()
        do {
            let result = try await work()
            let seconds = Date().timeIntervalSince(start).formatted(.number.precision(.fractionLength(1)))
            report = "\(name): \(result), \(seconds) s. The hive hummed \(hum - before) times meanwhile."
        } catch {
            report = "\(name) failed: \(error.localizedDescription)"
        }
        print(report)
    }
}

Run it and tap 100 pour slowly:

100 bees, one slow drop each: one drop in the jar — screenshot
100 bees, one slow drop each: one drop in the jarScreenshot · iPhone 17, iOS 27.0

One drop, not a hundred. Nothing is wrong with the actor: no two bees touched drops at the same instant. But an actor is free at every await inside it. All 100 bees ran the first line, saw 0, and went for honey; the jar was free each time; then each came back and wrote 0 + 1. Written out, the method is two jobs, and the jar belongs to nobody between them:

Jar.swift — pourSlowly, written out by hand, for comparison only
// pourSlowly(), as the compiler builds it: the method is cut at the await into two jobs for the jar.
// Between them the jar is free — so another bee's first job can run, and see the same `drops` you saw.
extension Jar {
    func pourSlowlyByHand() async {
        let seen = startPour()                           // job 1, in the jar
        try? await Task.sleep(for: .milliseconds(10))    // not in the jar: anyone may pour now
        finishPour(seen)                                 // job 2, in the jar again
    }

    func startPour() -> Int {
        drops
    }

    func finishPour(_ seen: Int) {
        drops = seen + 1
    }
}

This is the place the GCD bridge breaks. A block on a serial queue holds the queue until it returns; an actor method holds the actor only until its next await. The rule that follows: anything that must not be interrupted stays on one side of an await. Read, change and write in one stretch of synchronous code — as pour() does — and the actor keeps its promise.

Step 8

Where am I? isolated and #isolation

Every console line in this lesson named an actor, through whereAmI() in Where.swift. Lesson 1 handed you that function without explaining it. Here it is:

Where.swift
import Foundation

/// The actor this call is on: "MainActor", the name of an actor of yours, or "no actor". From lesson 1.
func whereAmI(_ isolation: isolated (any Actor)? = #isolation) -> String {
    isolation.map { "\(type(of: $0))" } ?? "no actor"
}

/// Whether this code is on the main thread. Only a synchronous function may ask — in `async` code the answer can
/// change at every `await`, and Swift 6 refuses the question there. On no actor: it answers for whoever calls it.
func onMainThread() -> Bool {
    Thread.isMainThread
}

isolated on a parameter means: run this function on that actor — the function has no isolation of its own, it takes its caller's. #isolation as the default value means: unless the caller passes an actor, use the one the caller is on. So whereAmI() with no argument is always answered with the actor of whoever called it. Add to Bees.swift — whereIsEveryone says @MainActor, so its own answer is the main actor's:

Add to Bees.swift
/// Asked from the pool: a @concurrent function is on no actor.
@concurrent func whereIsThePool() async -> String {
    whereAmI()
}

/// Where each of them is, asked from the main actor — and, written out beside each answer, what `#isolation`
/// filled in to get it.
@MainActor func whereIsEveryone(_ jar: Jar) async -> String {
    let fromHere = whereAmI()                       // #isolation → MainActor.shared
    let fromThePool = await whereIsThePool()        // #isolation → nil
    let fromTheJar = await jar.whereIsTheJar()      // #isolation → the jar
    let writtenOut = (whereAmI(MainActor.shared), whereAmI(nil), await whereAmI(jar))
    return "here: \(fromHere), pool: \(fromThePool), jar: \(fromTheJar); written out: \(writtenOut)"
}

And a button: add a fourth HStack, one more row of buttons, after the one with the jar's two buttons:

In ContentView.swift, replace the ContentView struct with this
struct ContentView: View {    @State private var hum = 0    @State private var bees: [Bee] = []    @State private var report = ""    @State private var trip: Task<Void, Never>?    @State private var jar = Jar()    var body: some View {        ZStack {            HiveArt(hum: hum, bees: bees)            VStack(spacing: 10) {                if !report.isEmpty {                    Text(report)                        .font(.system(.subheadline, design: .rounded).weight(.semibold))                        .multilineTextAlignment(.center)                        .padding(.horizontal, 14)                        .padding(.vertical, 8)                        .background(.white.opacity(0.9), in: RoundedRectangle(cornerRadius: 14))                }                HStack {                    Button("Send Ada to a flower") {                        Task {                            bees = [Bee(id: "Ada", place: "flying to the 🌻")]                            await errand("Ada's flight") { "\(try await fly()) bytes of nectar" }                            bees = [Bee(id: "Ada", place: "home")]                        }                    }                    Button("Count the pollen") {                        Task {                            await errand("Counting the pollen") { "\(await countPollen()) grains" }                        }                    }                }                HStack {                    Button("Send three bees") {                        trip = Task {                            bees = [Bee(id: "Ada", place: "at the 🌻"), Bee(id: "Bea", place: "at the 🌸"), Bee(id: "Cia", place: "at the 🌷")]                            await errand("Three bees") { await sendThree().joined(separator: "; ") }                            bees = []                        }                    }                    Button("Call them home") {                        trip?.cancel()                    }                }                HStack {                    Button("500 bees pour") {                        Task {                            await jar.empty()                            await errand("500 bees, two drops each") {                                await withTaskGroup(of: Void.self) { group in                                    for _ in 0..<500 {                                        group.addTask { await jar.pour() }                                    }                                }                                print(await jar.whereIsTheJar())                                return "\(await jar.drops) drops in the jar"                            }                        }                    }                    Button("100 pour slowly") {                        Task {                            await jar.empty()                            await errand("100 bees, one slow drop each") {                                await withTaskGroup(of: Void.self) { group in                                    for _ in 0..<100 {                                        group.addTask { await jar.pourSlowly() }                                    }                                }                                return "\(await jar.drops) drops in the jar"                            }                        }                    }                }                HStack {                    Button("Where is everyone?") {                        Task {                            report = await whereIsEveryone(jar)                            print(report)                        }                    }                }            }            .buttonStyle(.borderedProminent)            .tint(Color(red: 0.85, green: 0.55, blue: 0.1))            .frame(maxHeight: .infinity, alignment: .bottom)            .padding(.bottom, 24)            .padding(.horizontal, 20)        }        .task {            while !Task.isCancelled {                try? await Task.sleep(for: .milliseconds(100))                hum += 1            }        }    }    /// One errand: what it brought back, how long it took, and how often the hive hummed meanwhile.    func errand(_ name: String, _ work: () async throws -> String) async {        let before = hum, start = Date()        do {            let result = try await work()            let seconds = Date().timeIntervalSince(start).formatted(.number.precision(.fractionLength(1)))            report = "\(name): \(result), \(seconds) s. The hive hummed \(hum - before) times meanwhile."        } catch {            report = "\(name) failed: \(error.localizedDescription)"        }        print(report)    }}
struct ContentView: View {
    @State private var hum = 0
    @State private var bees: [Bee] = []
    @State private var report = ""
    @State private var trip: Task<Void, Never>?
    @State private var jar = Jar()

    var body: some View {
        ZStack {
            HiveArt(hum: hum, bees: bees)
            VStack(spacing: 10) {
                if !report.isEmpty {
                    Text(report)
                        .font(.system(.subheadline, design: .rounded).weight(.semibold))
                        .multilineTextAlignment(.center)
                        .padding(.horizontal, 14)
                        .padding(.vertical, 8)
                        .background(.white.opacity(0.9), in: RoundedRectangle(cornerRadius: 14))
                }
                HStack {
                    Button("Send Ada to a flower") {
                        Task {
                            bees = [Bee(id: "Ada", place: "flying to the 🌻")]
                            await errand("Ada's flight") { "\(try await fly()) bytes of nectar" }
                            bees = [Bee(id: "Ada", place: "home")]
                        }
                    }
                    Button("Count the pollen") {
                        Task {
                            await errand("Counting the pollen") { "\(await countPollen()) grains" }
                        }
                    }
                }
                HStack {
                    Button("Send three bees") {
                        trip = Task {
                            bees = [Bee(id: "Ada", place: "at the 🌻"), Bee(id: "Bea", place: "at the 🌸"), Bee(id: "Cia", place: "at the 🌷")]
                            await errand("Three bees") { await sendThree().joined(separator: "; ") }
                            bees = []
                        }
                    }
                    Button("Call them home") {
                        trip?.cancel()
                    }
                }
                HStack {
                    Button("500 bees pour") {
                        Task {
                            await jar.empty()
                            await errand("500 bees, two drops each") {
                                await withTaskGroup(of: Void.self) { group in
                                    for _ in 0..<500 {
                                        group.addTask { await jar.pour() }
                                    }
                                }
                                print(await jar.whereIsTheJar())
                                return "\(await jar.drops) drops in the jar"
                            }
                        }
                    }
                    Button("100 pour slowly") {
                        Task {
                            await jar.empty()
                            await errand("100 bees, one slow drop each") {
                                await withTaskGroup(of: Void.self) { group in
                                    for _ in 0..<100 {
                                        group.addTask { await jar.pourSlowly() }
                                    }
                                }
                                return "\(await jar.drops) drops in the jar"
                            }
                        }
                    }
                }
                HStack {
                    Button("Where is everyone?") {
                        Task {
                            report = await whereIsEveryone(jar)
                            print(report)
                        }
                    }
                }
            }
            .buttonStyle(.borderedProminent)
            .tint(Color(red: 0.85, green: 0.55, blue: 0.1))
            .frame(maxHeight: .infinity, alignment: .bottom)
            .padding(.bottom, 24)
            .padding(.horizontal, 20)
        }
        .task {
            while !Task.isCancelled {
                try? await Task.sleep(for: .milliseconds(100))
                hum += 1
            }
        }
    }

    /// One errand: what it brought back, how long it took, and how often the hive hummed meanwhile.
    func errand(_ name: String, _ work: () async throws -> String) async {
        let before = hum, start = Date()
        do {
            let result = try await work()
            let seconds = Date().timeIntervalSince(start).formatted(.number.precision(.fractionLength(1)))
            report = "\(name): \(result), \(seconds) s. The hive hummed \(hum - before) times meanwhile."
        } catch {
            report = "\(name) failed: \(error.localizedDescription)"
        }
        print(report)
    }
}

Run it and ask:

Three answers, and the three written-out calls that give the same three — screenshot
Three answers, and the three written-out calls that give the same threeScreenshot · iPhone 17, iOS 27.0
Console
here: MainActor, pool: no actor, jar: the jar is on Jar; written out: ("MainActor", "no actor", "Jar")

#isolation written out is just the caller's actor as an argument:

Bees.swift — #isolation, written out by hand, for comparison only
// #isolation, written out: the compiler puts the caller's own actor into the argument.

/// whereAmI(), called from a view or any main-actor code.
@MainActor func whereIsHereByHand() -> String {
    whereAmI(MainActor.shared)
}

/// whereAmI(), called from a @concurrent function, as whereIsThePool() does.
@concurrent func whereIsThePoolByHand() async -> String {
    whereAmI(nil)
}

extension Jar {
    /// whereAmI(), called from inside one of the jar's methods, as whereIsTheJar() does.
    func whereIsTheJarByHand() -> String {
        whereAmI(self)
    }
}

Pass a different actor and the function really runs there: whereAmI(jar) from the view hopped into the jar to answer "Jar". That is what isolated gives you — a function that goes to whatever actor you hand it — and #isolation is how a library function follows its caller without asking.

Step 9

A thousand bees: what a task costs

Every errand so far was one task, or three. Tasks are meant to be had by the thousand. Add to Bees.swift:

Add to Bees.swift
/// The number the system gives the thread this code runs on. Synchronous, so that it may ask; on no actor, so
/// that it answers for whoever calls it.
func threadNumber() -> UInt32 {
    pthread_mach_thread_np(pthread_self())
}

/// One bee of the swarm: a second at a flower, on the main actor. The thread she comes back on.
@MainActor func bee() async -> UInt32 {
    try? await Task.sleep(for: .seconds(1))
    return threadNumber()
}

/// A thousand bees, each a task of her own on the main actor, each a second at a flower, all at once.
@MainActor func swarm() async -> String {
    var threads = Set<UInt32>()
    await withTaskGroup(of: UInt32.self) { group in
        for _ in 0..<1_000 {
            group.addTask { await bee() }       // a child task, flying bee(): the main actor's
        }
        for await thread in group {
            threads.insert(thread)
        }
    }
    return "1000 bees on the main actor, back on \(threads.count) thread"
}

/// Ten thousand bees on the shared pool. Each notes the thread she left from and the thread she came back on.
@concurrent func poolSwarm() async -> String {
    var threads = Set<UInt32>()
    var moved = 0
    await withTaskGroup(of: (UInt32, UInt32).self) { group in
        for _ in 0..<10_000 {
            group.addTask { @concurrent in
                let left = threadNumber()
                try? await Task.sleep(for: .seconds(1))
                return (left, threadNumber())
            }
        }
        for await (left, back) in group {
            threads.formUnion([left, back])
            if left != back {
                moved += 1
            }
        }
    }
    let cores = ProcessInfo.processInfo.activeProcessorCount
    return "10000 bees on the pool, on \(threads.count) threads of a \(cores)-core Mac; \(moved) came back on another thread than they left"
}

swarm() sends a thousand bees at once, each a task of her own that flies bee() — a second at a flower, on the main actor, because bee() says @MainActor — and collects the number of the thread each came back on. poolSwarm() sends ten thousand onto the shared pool — @concurrent, the function and each bee — and each notes the thread she left from and the one she came back on. threadNumber() asks the system which thread is running it; it is synchronous, because only synchronous code may ask, and it says no actor, so that it answers for whoever calls it. Now a row of buttons: add a fifth HStack, after the one with Where is everyone?:

In ContentView.swift, replace the ContentView struct with this
struct ContentView: View {    @State private var hum = 0    @State private var bees: [Bee] = []    @State private var report = ""    @State private var trip: Task<Void, Never>?    @State private var jar = Jar()    var body: some View {        ZStack {            HiveArt(hum: hum, bees: bees)            VStack(spacing: 10) {                if !report.isEmpty {                    Text(report)                        .font(.system(.subheadline, design: .rounded).weight(.semibold))                        .multilineTextAlignment(.center)                        .padding(.horizontal, 14)                        .padding(.vertical, 8)                        .background(.white.opacity(0.9), in: RoundedRectangle(cornerRadius: 14))                }                HStack {                    Button("Send Ada to a flower") {                        Task {                            bees = [Bee(id: "Ada", place: "flying to the 🌻")]                            await errand("Ada's flight") { "\(try await fly()) bytes of nectar" }                            bees = [Bee(id: "Ada", place: "home")]                        }                    }                    Button("Count the pollen") {                        Task {                            await errand("Counting the pollen") { "\(await countPollen()) grains" }                        }                    }                }                HStack {                    Button("Send three bees") {                        trip = Task {                            bees = [Bee(id: "Ada", place: "at the 🌻"), Bee(id: "Bea", place: "at the 🌸"), Bee(id: "Cia", place: "at the 🌷")]                            await errand("Three bees") { await sendThree().joined(separator: "; ") }                            bees = []                        }                    }                    Button("Call them home") {                        trip?.cancel()                    }                }                HStack {                    Button("500 bees pour") {                        Task {                            await jar.empty()                            await errand("500 bees, two drops each") {                                await withTaskGroup(of: Void.self) { group in                                    for _ in 0..<500 {                                        group.addTask { await jar.pour() }                                    }                                }                                print(await jar.whereIsTheJar())                                return "\(await jar.drops) drops in the jar"                            }                        }                    }                    Button("100 pour slowly") {                        Task {                            await jar.empty()                            await errand("100 bees, one slow drop each") {                                await withTaskGroup(of: Void.self) { group in                                    for _ in 0..<100 {                                        group.addTask { await jar.pourSlowly() }                                    }                                }                                return "\(await jar.drops) drops in the jar"                            }                        }                    }                }                HStack {                    Button("Where is everyone?") {                        Task {                            report = await whereIsEveryone(jar)                            print(report)                        }                    }                }                HStack {                    Button("1,000 bees") {                        Task {                            await errand("A thousand bees") { await swarm() }                        }                    }                    Button("10,000 bees") {                        Task {                            await errand("Ten thousand bees") { await poolSwarm() }                        }                    }                }            }            .buttonStyle(.borderedProminent)            .tint(Color(red: 0.85, green: 0.55, blue: 0.1))            .frame(maxHeight: .infinity, alignment: .bottom)            .padding(.bottom, 24)            .padding(.horizontal, 20)        }        .task {            while !Task.isCancelled {                try? await Task.sleep(for: .milliseconds(100))                hum += 1            }        }    }    /// One errand: what it brought back, how long it took, and how often the hive hummed meanwhile.    func errand(_ name: String, _ work: () async throws -> String) async {        let before = hum, start = Date()        do {            let result = try await work()            let seconds = Date().timeIntervalSince(start).formatted(.number.precision(.fractionLength(1)))            report = "\(name): \(result), \(seconds) s. The hive hummed \(hum - before) times meanwhile."        } catch {            report = "\(name) failed: \(error.localizedDescription)"        }        print(report)    }}
struct ContentView: View {
    @State private var hum = 0
    @State private var bees: [Bee] = []
    @State private var report = ""
    @State private var trip: Task<Void, Never>?
    @State private var jar = Jar()

    var body: some View {
        ZStack {
            HiveArt(hum: hum, bees: bees)
            VStack(spacing: 10) {
                if !report.isEmpty {
                    Text(report)
                        .font(.system(.subheadline, design: .rounded).weight(.semibold))
                        .multilineTextAlignment(.center)
                        .padding(.horizontal, 14)
                        .padding(.vertical, 8)
                        .background(.white.opacity(0.9), in: RoundedRectangle(cornerRadius: 14))
                }
                HStack {
                    Button("Send Ada to a flower") {
                        Task {
                            bees = [Bee(id: "Ada", place: "flying to the 🌻")]
                            await errand("Ada's flight") { "\(try await fly()) bytes of nectar" }
                            bees = [Bee(id: "Ada", place: "home")]
                        }
                    }
                    Button("Count the pollen") {
                        Task {
                            await errand("Counting the pollen") { "\(await countPollen()) grains" }
                        }
                    }
                }
                HStack {
                    Button("Send three bees") {
                        trip = Task {
                            bees = [Bee(id: "Ada", place: "at the 🌻"), Bee(id: "Bea", place: "at the 🌸"), Bee(id: "Cia", place: "at the 🌷")]
                            await errand("Three bees") { await sendThree().joined(separator: "; ") }
                            bees = []
                        }
                    }
                    Button("Call them home") {
                        trip?.cancel()
                    }
                }
                HStack {
                    Button("500 bees pour") {
                        Task {
                            await jar.empty()
                            await errand("500 bees, two drops each") {
                                await withTaskGroup(of: Void.self) { group in
                                    for _ in 0..<500 {
                                        group.addTask { await jar.pour() }
                                    }
                                }
                                print(await jar.whereIsTheJar())
                                return "\(await jar.drops) drops in the jar"
                            }
                        }
                    }
                    Button("100 pour slowly") {
                        Task {
                            await jar.empty()
                            await errand("100 bees, one slow drop each") {
                                await withTaskGroup(of: Void.self) { group in
                                    for _ in 0..<100 {
                                        group.addTask { await jar.pourSlowly() }
                                    }
                                }
                                return "\(await jar.drops) drops in the jar"
                            }
                        }
                    }
                }
                HStack {
                    Button("Where is everyone?") {
                        Task {
                            report = await whereIsEveryone(jar)
                            print(report)
                        }
                    }
                }
                HStack {
                    Button("1,000 bees") {
                        Task {
                            await errand("A thousand bees") { await swarm() }
                        }
                    }
                    Button("10,000 bees") {
                        Task {
                            await errand("Ten thousand bees") { await poolSwarm() }
                        }
                    }
                }
            }
            .buttonStyle(.borderedProminent)
            .tint(Color(red: 0.85, green: 0.55, blue: 0.1))
            .frame(maxHeight: .infinity, alignment: .bottom)
            .padding(.bottom, 24)
            .padding(.horizontal, 20)
        }
        .task {
            while !Task.isCancelled {
                try? await Task.sleep(for: .milliseconds(100))
                hum += 1
            }
        }
    }

    /// One errand: what it brought back, how long it took, and how often the hive hummed meanwhile.
    func errand(_ name: String, _ work: () async throws -> String) async {
        let before = hum, start = Date()
        do {
            let result = try await work()
            let seconds = Date().timeIntervalSince(start).formatted(.number.precision(.fractionLength(1)))
            report = "\(name): \(result), \(seconds) s. The hive hummed \(hum - before) times meanwhile."
        } catch {
            report = "\(name) failed: \(error.localizedDescription)"
        }
        print(report)
    }
}

Run it and tap 1,000 bees:

A thousand bees, a second each: about a second in all, on one thread — screenshot
A thousand bees, a second each: about a second in all, on one threadScreenshot · iPhone 17, iOS 27.0
Console
A thousand bees: 1000 bees on the main actor, back on 1 thread, 1.0 s. The hive hummed 10 times meanwhile.

A thousand tasks, each waiting a second, all done in about a second — and all on one thread: the main thread, the only one the main actor has. The hive hummed right through. That is what "a task does not hold a thread while it waits" comes to: while the thousand were at their flowers, nothing of theirs was running anywhere, and the one thread was free to hum. Now tap 10,000 bees:

Ten thousand on the pool: about a second again, on a handful of threads — screenshot
Ten thousand on the pool: about a second again, on a handful of threadsScreenshot · iPhone 17, iOS 27.0
Console
Ten thousand bees: 10000 bees on the pool, on 12 threads of a 10-core Mac; 9137 came back on another thread than they left, 1.0 s. The hive hummed 10 times meanwhile.

Ten thousand tasks, and the pool used about as many threads as the Mac has cores. That is its design: the pool is sized to the machine, not to the work, because a thread is only useful while it has something to run, and a task gives its thread up at every await — so a handful of threads is enough for any number of tasks. And most of the bees came back on a different thread from the one they left. The task is what carries on; the thread is whichever one is free when there is something to run.

So when does a different thread pick a task up? Only off the main actor. The main actor has one thread, the main thread, so the continuation of main-actor code always runs there — all thousand bees of the first swarm came back on it. Code on no actor, as the second swarm was, goes back to whichever thread of the pool is free; so does an actor of your own, like the jar, which runs one job at a time but on any thread of the pool. Apple's warning that the thread before an await need not be the one after it is about those. What holds on the main actor too is the warning's other half: an await breaks atomicity. The main thread does other work while you wait — it moved the hum — so anything you read before an await may have changed after it. Step 7 lost 99 drops of honey that way.

Both swarms are task groups. withTaskGroup makes a group whose children are tasks of the task that called it — structured, like async let, but as many as you like, decided while the code runs. addTask starts a child at once. for await takes the children's results in the order they finish, not the order they were added — one of this lesson's cards shows it. And the group does not return until every child has finished, so no child can outlive it; cancel the task that runs the group, and every child is cancelled. async let is the same thing for a fixed, named few: step 5 wrote it out as a group. withThrowingTaskGroup is the version whose children may throw — lesson 38 fetches the weather of every trip with one.

So what is a task? Not a thread, and not a closure. It is a small object that the Swift runtime makes on the heap — for Task { }, for each async let, for each addTask — and keeps until the work is done. In it are the piece of your code to run next; the frames of the functions that are paused, with their local variables; the result, once there is one; and the task's priority, its cancellation flag and its task-local values. The Task value you keep, as the view keeps trip, is eight bytes: a pointer to that object — one of this lesson's cards measures it. And running a task is not a thread's job but an executor's: the main actor's executor runs the pieces of its tasks on the main thread, one at a time, and the pool runs every other piece on its threads. Step 10 opens a task up.

The bridge from GCD

A block submitted to a queue holds a thread from its first line to its last; a block that waits blocks that thread, and GCD makes more threads to keep the queue moving — the "thread explosion" that a thousand waiting blocks can cause. A thousand waiting tasks hold no threads at all, and the pool never grows past the machine.

Where this comes from: WWDC21: Swift concurrency: Behind the scenes (2021 — the thread model is unchanged)

Step 10

Inside a waiting bee: the pieces and the frame

Step 2 said that the compiler cuts fly() in two at the await, and keeps its locals in a frame off the thread. Both can be watched. Make a new Swift file named Inside, and make it read — it is ready-made, like HiveArt, and it is for looking, never for shipping:

New fileInside.swift — the whole file
import Foundation

// Ready-made: two tools for looking inside a running app. For looking, never for shipping: they read the Swift
// runtime's private layout and the app's own symbols, and either can change with any release of iOS.

/// Whether `value` is anywhere in a task's memory: in the task object a `Task` points to, or in any block of memory
/// the task object points to.
func taskMemory<Success, Failure>(of task: Task<Success, Failure>, contains value: UInt64) -> Bool {
    let object = unsafeBitCast(task, to: UnsafeRawPointer.self)
    var blocks = [object]
    for i in 0..<(malloc_size(object) / 8) {
        let word = object.load(fromByteOffset: i * 8, as: UInt.self)
        if let block = UnsafeRawPointer(bitPattern: word), malloc_size(block) > 0 {
            blocks.append(block)
        }
    }
    return blocks.contains { block in
        (0..<(malloc_size(block) / 8)).contains { block.load(fromByteOffset: $0 * 8, as: UInt64.self) == value }
    }
}

/// The piece of code that called this function, by the name the compiler gave it, with the app's name left off.
func pieceRunning() -> String {
    let caller = Thread.callStackSymbols[1].split(separator: " ", omittingEmptySubsequences: true)
    let mangled = String(caller[3])
    guard let name = swift_demangle(mangled, UInt(mangled.utf8.count), nil, nil, 0) else {
        return mangled
    }
    defer {
        free(name)
    }
    // "Hive", the app's module
    let app = #fileID.split(separator: "/")[0]
    return String(cString: name).replacingOccurrences(of: "\(app).", with: "")
}

/// Swift's own demangler, which turns a symbol's name back into Swift.
@_silgen_name("swift_demangle")
private func swift_demangle(_ name: UnsafePointer<CChar>?, _ length: UInt, _ buffer: UnsafeMutablePointer<CChar>?,
                            _ size: UnsafeMutablePointer<UInt>?, _ flags: UInt32) -> UnsafeMutablePointer<CChar>?

taskMemory(of:contains:) searches a task for a number: the object a Task points to, and every block of memory that object points to. pieceRunning() names the code that called it, by the name the compiler gave that code in the app: Thread.callStackSymbols lists the names, and swift_demangle, the Swift runtime's own, turns them back into Swift. Now the bee. Add to Bees.swift:

Add to Bees.swift
/// Two marks for Eve to carry: one she needs after her wait, and one she is done with before it.
/// `@MainActor`: a global `var` must say whose it is, and Eve reads them on the main actor.
@MainActor var keptMark: UInt64 = 0xFEED_FACE_CAFE_BEEF
@MainActor var spentMark: UInt64 = 0xDEAD_BEEF_0BAD_F00D

/// Eve flies to a flower, from the main actor. While she waits there, look inside her task.
@MainActor func lookInside() async -> String {
    let eve = Task {
        let kept = keptMark
        let spent = spentMark
        print("before her wait, Eve runs as: \(pieceRunning())")
        print("the mark she is done with: \(String(spent, radix: 16))")
        try? await Task.sleep(for: .seconds(1))
        print("after her wait, Eve runs as: \(pieceRunning())")
        print("the mark she needed: \(String(kept, radix: 16))")
    }
    // Eve is at her flower: her task is waiting
    try? await Task.sleep(for: .milliseconds(300))
    let kept = taskMemory(of: eve, contains: keptMark)
    let spent = taskMemory(of: eve, contains: spentMark)
    await eve.value
    return "while Eve waited, her task held the mark she needed: \(kept ? "yes" : "no"); the one she was done with: \(spent ? "yes" : "no")"
}

Eve carries two marks — two numbers that are easy to find in memory: one she needs after her wait, and one she is done with before it. They are global variables, and Swift 6 asks a global var whose it is: these are the main actor's, where Eve reads them. While she waits at her flower, lookInside() searches her task for both. Give her a button: add a third button to the swarm's HStack, after 10,000 bees:

In ContentView.swift, replace the ContentView struct with this
struct ContentView: View {    @State private var hum = 0    @State private var bees: [Bee] = []    @State private var report = ""    @State private var trip: Task<Void, Never>?    @State private var jar = Jar()    var body: some View {        ZStack {            HiveArt(hum: hum, bees: bees)            VStack(spacing: 10) {                if !report.isEmpty {                    Text(report)                        .font(.system(.subheadline, design: .rounded).weight(.semibold))                        .multilineTextAlignment(.center)                        .padding(.horizontal, 14)                        .padding(.vertical, 8)                        .background(.white.opacity(0.9), in: RoundedRectangle(cornerRadius: 14))                }                HStack {                    Button("Send Ada to a flower") {                        Task {                            bees = [Bee(id: "Ada", place: "flying to the 🌻")]                            await errand("Ada's flight") { "\(try await fly()) bytes of nectar" }                            bees = [Bee(id: "Ada", place: "home")]                        }                    }                    Button("Count the pollen") {                        Task {                            await errand("Counting the pollen") { "\(await countPollen()) grains" }                        }                    }                }                HStack {                    Button("Send three bees") {                        trip = Task {                            bees = [Bee(id: "Ada", place: "at the 🌻"), Bee(id: "Bea", place: "at the 🌸"), Bee(id: "Cia", place: "at the 🌷")]                            await errand("Three bees") { await sendThree().joined(separator: "; ") }                            bees = []                        }                    }                    Button("Call them home") {                        trip?.cancel()                    }                }                HStack {                    Button("500 bees pour") {                        Task {                            await jar.empty()                            await errand("500 bees, two drops each") {                                await withTaskGroup(of: Void.self) { group in                                    for _ in 0..<500 {                                        group.addTask { await jar.pour() }                                    }                                }                                print(await jar.whereIsTheJar())                                return "\(await jar.drops) drops in the jar"                            }                        }                    }                    Button("100 pour slowly") {                        Task {                            await jar.empty()                            await errand("100 bees, one slow drop each") {                                await withTaskGroup(of: Void.self) { group in                                    for _ in 0..<100 {                                        group.addTask { await jar.pourSlowly() }                                    }                                }                                return "\(await jar.drops) drops in the jar"                            }                        }                    }                }                HStack {                    Button("Where is everyone?") {                        Task {                            report = await whereIsEveryone(jar)                            print(report)                        }                    }                }                HStack {                    Button("1,000 bees") {                        Task {                            await errand("A thousand bees") { await swarm() }                        }                    }                    Button("10,000 bees") {                        Task {                            await errand("Ten thousand bees") { await poolSwarm() }                        }                    }                    Button("Look inside") {                        Task {                            await errand("Eve's flight") { await lookInside() }                        }                    }                }            }            .buttonStyle(.borderedProminent)            .tint(Color(red: 0.85, green: 0.55, blue: 0.1))            .frame(maxHeight: .infinity, alignment: .bottom)            .padding(.bottom, 24)            .padding(.horizontal, 20)        }        .task {            while !Task.isCancelled {                try? await Task.sleep(for: .milliseconds(100))                hum += 1            }        }    }    /// One errand: what it brought back, how long it took, and how often the hive hummed meanwhile.    func errand(_ name: String, _ work: () async throws -> String) async {        let before = hum, start = Date()        do {            let result = try await work()            let seconds = Date().timeIntervalSince(start).formatted(.number.precision(.fractionLength(1)))            report = "\(name): \(result), \(seconds) s. The hive hummed \(hum - before) times meanwhile."        } catch {            report = "\(name) failed: \(error.localizedDescription)"        }        print(report)    }}
struct ContentView: View {
    @State private var hum = 0
    @State private var bees: [Bee] = []
    @State private var report = ""
    @State private var trip: Task<Void, Never>?
    @State private var jar = Jar()

    var body: some View {
        ZStack {
            HiveArt(hum: hum, bees: bees)
            VStack(spacing: 10) {
                if !report.isEmpty {
                    Text(report)
                        .font(.system(.subheadline, design: .rounded).weight(.semibold))
                        .multilineTextAlignment(.center)
                        .padding(.horizontal, 14)
                        .padding(.vertical, 8)
                        .background(.white.opacity(0.9), in: RoundedRectangle(cornerRadius: 14))
                }
                HStack {
                    Button("Send Ada to a flower") {
                        Task {
                            bees = [Bee(id: "Ada", place: "flying to the 🌻")]
                            await errand("Ada's flight") { "\(try await fly()) bytes of nectar" }
                            bees = [Bee(id: "Ada", place: "home")]
                        }
                    }
                    Button("Count the pollen") {
                        Task {
                            await errand("Counting the pollen") { "\(await countPollen()) grains" }
                        }
                    }
                }
                HStack {
                    Button("Send three bees") {
                        trip = Task {
                            bees = [Bee(id: "Ada", place: "at the 🌻"), Bee(id: "Bea", place: "at the 🌸"), Bee(id: "Cia", place: "at the 🌷")]
                            await errand("Three bees") { await sendThree().joined(separator: "; ") }
                            bees = []
                        }
                    }
                    Button("Call them home") {
                        trip?.cancel()
                    }
                }
                HStack {
                    Button("500 bees pour") {
                        Task {
                            await jar.empty()
                            await errand("500 bees, two drops each") {
                                await withTaskGroup(of: Void.self) { group in
                                    for _ in 0..<500 {
                                        group.addTask { await jar.pour() }
                                    }
                                }
                                print(await jar.whereIsTheJar())
                                return "\(await jar.drops) drops in the jar"
                            }
                        }
                    }
                    Button("100 pour slowly") {
                        Task {
                            await jar.empty()
                            await errand("100 bees, one slow drop each") {
                                await withTaskGroup(of: Void.self) { group in
                                    for _ in 0..<100 {
                                        group.addTask { await jar.pourSlowly() }
                                    }
                                }
                                return "\(await jar.drops) drops in the jar"
                            }
                        }
                    }
                }
                HStack {
                    Button("Where is everyone?") {
                        Task {
                            report = await whereIsEveryone(jar)
                            print(report)
                        }
                    }
                }
                HStack {
                    Button("1,000 bees") {
                        Task {
                            await errand("A thousand bees") { await swarm() }
                        }
                    }
                    Button("10,000 bees") {
                        Task {
                            await errand("Ten thousand bees") { await poolSwarm() }
                        }
                    }
                    Button("Look inside") {
                        Task {
                            await errand("Eve's flight") { await lookInside() }
                        }
                    }
                }
            }
            .buttonStyle(.borderedProminent)
            .tint(Color(red: 0.85, green: 0.55, blue: 0.1))
            .frame(maxHeight: .infinity, alignment: .bottom)
            .padding(.bottom, 24)
            .padding(.horizontal, 20)
        }
        .task {
            while !Task.isCancelled {
                try? await Task.sleep(for: .milliseconds(100))
                hum += 1
            }
        }
    }

    /// One errand: what it brought back, how long it took, and how often the hive hummed meanwhile.
    func errand(_ name: String, _ work: () async throws -> String) async {
        let before = hum, start = Date()
        do {
            let result = try await work()
            let seconds = Date().timeIntervalSince(start).formatted(.number.precision(.fractionLength(1)))
            report = "\(name): \(result), \(seconds) s. The hive hummed \(hum - before) times meanwhile."
        } catch {
            report = "\(name) failed: \(error.localizedDescription)"
        }
        print(report)
    }
}

Run it and tap Look inside:

Eve at her flower, and her task looked inside — screenshot
Eve at her flower, and her task looked insideScreenshot · iPhone 17, iOS 27.0
Console
before her wait, Eve runs as: (1) suspend resume partial function for closure #1 () async -> () in lookInside() async -> Swift.String
the mark she is done with: deadbeef0badf00d
after her wait, Eve runs as: (3) suspend resume partial function for closure #1 () async -> () in lookInside() async -> Swift.String
the mark she needed: feedfacecafebeef
Eve's flight: while Eve waited, her task held the mark she needed: yes; the one she was done with: yes, 1.0 s. The hive hummed 10 times meanwhile.

Two things are on that console. The pieces. Before her wait, Eve ran as one function, (1) suspend resume partial function for closure #1; after it, as another, (3) suspend resume partial function. The compiler cut her closure into numbered pieces at its suspension points — as you cut fly() by hand in step 2 — and the runtime ran one piece, then, a second later, another. Between them nothing of Eve's was running. The frame. While she waited, both her marks were in her task's memory — not on any thread's stack, because she was on no thread. A task keeps the frames of its paused functions as a thread keeps stack frames, except that they are the task's: allocated from the task's own memory, so that they outlive the piece that made them and the thread that ran it.

While Eve runs: the piece before her awaitTHE MAIN THREAD’S STACKEVE’S TASK, ON THE HEAPpieceRunning(), print(…)piece (1) of Eve’s closurethe main actor’s executorthe run looprunning, on the main threadEve’s frame: kept, spentresult: not yetcancelled flag, priorityreads andwrites her frameWhile Eve waits: nothing of hers is on any threadTHE MAIN THREAD’S STACKEVE’S TASK, ON THE HEAPno piece of Eve’sa piece of the hum’s taskthe main actor’s executorthe run loopnext piece: (3) of Eve’s closureEve’s frame: kept, spentTask.sleep’s own framesresult, cancelled flag, prioritywhen the second is up,it runs on a thread again
Eve, running and waiting. While she runs, one piece of her closure is on the main thread's stack, working on her frame in the task. While she waits, nothing of hers is on any thread: her task holds her frame and the piece to run next, and the main thread runs other work — the hum.

That pair — the next piece, and the frames it will need — has a name. Apple's talk on the machinery, Swift concurrency: Behind the scenes, says that "a function can be broken up into continuations at an await". A continuation is the rest of the function after an await: for Eve, piece (3) and her frame. It is what a task keeps on the heap while it waits — the talk calls the list of async frames the runtime's representation of a continuation — and resuming a task means running its continuation, on whatever thread is free. You can even hold one: withCheckedContinuation, which lesson 40 uses to wrap a delegate, suspends the task and hands you its continuation as a value; call resume on it, and the task carries on.

One detail of the frame depends on the build. Product ▸ Run makes a Debug build, which keeps every local in the frame where the debugger can show it — so both marks were there. An optimized build has to keep in the frame only what a later piece reads: in the Release build of one of this lesson's edge cases, the mark Eve needed was in her task, and the one she was done with was not — unless she printed it, which left a copy in a temporary in the frame. A value used only between two awaits can live on the thread's stack, and its piece gives it back when it ends.

And here is the whole of it, as the runtime does it. Task { } is a call to swift_task_create, which makes the task and the frame of its first function. An executor runs the first piece on a thread. The piece runs until an await that really waits; there it stores its continuation in the task — the next piece, and the frames — and returns, and the thread is free. When the wait is over, the task goes back to an executor, and the next piece runs on whatever thread that executor has. A hop to another actor is a call to swift_task_switch: the piece ends, and its continuation is handed to that actor's executor. One of this lesson's edge cases finds both calls in the compiled code. That is all await is.

Step 11

Under it all: GCD?

One question is left from the bridge notes: is GCD under all of this? Ask each kind of code which dispatch queue is running it. Add to Bees.swift:

Add to Bees.swift
/// The label of the dispatch queue running this code. Synchronous, so that it may ask; on no actor, so that it
/// answers for whoever calls it.
func queueName() -> String {
    String(cString: __dispatch_queue_get_label(nil))
}

/// Asked from the pool.
@concurrent func queueOfThePool() async -> String {
    queueName()
}

extension Jar {
    /// Asked from inside the jar.
    func queueOfTheJar() -> String {
        queueName()
    }
}

/// Which dispatch queue runs main-actor code, @concurrent code, the jar, and a block sent with GCD — asked from the
/// main actor.
@MainActor func whoRunsWhat(_ jar: Jar) async -> String {
    let main = queueName()
    let pool = await queueOfThePool()
    let inTheJar = await jar.queueOfTheJar()
    let gcd = await withCheckedContinuation { done in
        DispatchQueue.global(qos: .userInitiated).async {
            done.resume(returning: queueName())
        }
    }
    return "main actor: \(main)\n@concurrent: \(pool)\nthe jar: \(inTheJar)\nDispatchQueue.global: \(gcd)"
}

queueName() asks GCD for the label of the queue it is on — synchronous, like threadNumber(). whoRunsWhat asks from the main actor, from a @concurrent function, from inside the jar, and from a block sent the GCD way, DispatchQueue.global().async, whose answer comes back through a continuation. A button for it, beside Where is everyone?:

In ContentView.swift, replace the ContentView struct with this
struct ContentView: View {    @State private var hum = 0    @State private var bees: [Bee] = []    @State private var report = ""    @State private var trip: Task<Void, Never>?    @State private var jar = Jar()    var body: some View {        ZStack {            HiveArt(hum: hum, bees: bees)            VStack(spacing: 10) {                if !report.isEmpty {                    Text(report)                        .font(.system(.subheadline, design: .rounded).weight(.semibold))                        .multilineTextAlignment(.center)                        .padding(.horizontal, 14)                        .padding(.vertical, 8)                        .background(.white.opacity(0.9), in: RoundedRectangle(cornerRadius: 14))                }                HStack {                    Button("Send Ada to a flower") {                        Task {                            bees = [Bee(id: "Ada", place: "flying to the 🌻")]                            await errand("Ada's flight") { "\(try await fly()) bytes of nectar" }                            bees = [Bee(id: "Ada", place: "home")]                        }                    }                    Button("Count the pollen") {                        Task {                            await errand("Counting the pollen") { "\(await countPollen()) grains" }                        }                    }                }                HStack {                    Button("Send three bees") {                        trip = Task {                            bees = [Bee(id: "Ada", place: "at the 🌻"), Bee(id: "Bea", place: "at the 🌸"), Bee(id: "Cia", place: "at the 🌷")]                            await errand("Three bees") { await sendThree().joined(separator: "; ") }                            bees = []                        }                    }                    Button("Call them home") {                        trip?.cancel()                    }                }                HStack {                    Button("500 bees pour") {                        Task {                            await jar.empty()                            await errand("500 bees, two drops each") {                                await withTaskGroup(of: Void.self) { group in                                    for _ in 0..<500 {                                        group.addTask { await jar.pour() }                                    }                                }                                print(await jar.whereIsTheJar())                                return "\(await jar.drops) drops in the jar"                            }                        }                    }                    Button("100 pour slowly") {                        Task {                            await jar.empty()                            await errand("100 bees, one slow drop each") {                                await withTaskGroup(of: Void.self) { group in                                    for _ in 0..<100 {                                        group.addTask { await jar.pourSlowly() }                                    }                                }                                return "\(await jar.drops) drops in the jar"                            }                        }                    }                }                HStack {                    Button("Where is everyone?") {                        Task {                            report = await whereIsEveryone(jar)                            print(report)                        }                    }                    Button("Which queue?") {                        Task {                            report = await whoRunsWhat(jar)                            print(report)                        }                    }                }                HStack {                    Button("1,000 bees") {                        Task {                            await errand("A thousand bees") { await swarm() }                        }                    }                    Button("10,000 bees") {                        Task {                            await errand("Ten thousand bees") { await poolSwarm() }                        }                    }                    Button("Look inside") {                        Task {                            await errand("Eve's flight") { await lookInside() }                        }                    }                }            }            .buttonStyle(.borderedProminent)            .tint(Color(red: 0.85, green: 0.55, blue: 0.1))            .frame(maxHeight: .infinity, alignment: .bottom)            .padding(.bottom, 24)            .padding(.horizontal, 20)        }        .task {            while !Task.isCancelled {                try? await Task.sleep(for: .milliseconds(100))                hum += 1            }        }    }    /// One errand: what it brought back, how long it took, and how often the hive hummed meanwhile.    func errand(_ name: String, _ work: () async throws -> String) async {        let before = hum, start = Date()        do {            let result = try await work()            let seconds = Date().timeIntervalSince(start).formatted(.number.precision(.fractionLength(1)))            report = "\(name): \(result), \(seconds) s. The hive hummed \(hum - before) times meanwhile."        } catch {            report = "\(name) failed: \(error.localizedDescription)"        }        print(report)    }}
struct ContentView: View {
    @State private var hum = 0
    @State private var bees: [Bee] = []
    @State private var report = ""
    @State private var trip: Task<Void, Never>?
    @State private var jar = Jar()

    var body: some View {
        ZStack {
            HiveArt(hum: hum, bees: bees)
            VStack(spacing: 10) {
                if !report.isEmpty {
                    Text(report)
                        .font(.system(.subheadline, design: .rounded).weight(.semibold))
                        .multilineTextAlignment(.center)
                        .padding(.horizontal, 14)
                        .padding(.vertical, 8)
                        .background(.white.opacity(0.9), in: RoundedRectangle(cornerRadius: 14))
                }
                HStack {
                    Button("Send Ada to a flower") {
                        Task {
                            bees = [Bee(id: "Ada", place: "flying to the 🌻")]
                            await errand("Ada's flight") { "\(try await fly()) bytes of nectar" }
                            bees = [Bee(id: "Ada", place: "home")]
                        }
                    }
                    Button("Count the pollen") {
                        Task {
                            await errand("Counting the pollen") { "\(await countPollen()) grains" }
                        }
                    }
                }
                HStack {
                    Button("Send three bees") {
                        trip = Task {
                            bees = [Bee(id: "Ada", place: "at the 🌻"), Bee(id: "Bea", place: "at the 🌸"), Bee(id: "Cia", place: "at the 🌷")]
                            await errand("Three bees") { await sendThree().joined(separator: "; ") }
                            bees = []
                        }
                    }
                    Button("Call them home") {
                        trip?.cancel()
                    }
                }
                HStack {
                    Button("500 bees pour") {
                        Task {
                            await jar.empty()
                            await errand("500 bees, two drops each") {
                                await withTaskGroup(of: Void.self) { group in
                                    for _ in 0..<500 {
                                        group.addTask { await jar.pour() }
                                    }
                                }
                                print(await jar.whereIsTheJar())
                                return "\(await jar.drops) drops in the jar"
                            }
                        }
                    }
                    Button("100 pour slowly") {
                        Task {
                            await jar.empty()
                            await errand("100 bees, one slow drop each") {
                                await withTaskGroup(of: Void.self) { group in
                                    for _ in 0..<100 {
                                        group.addTask { await jar.pourSlowly() }
                                    }
                                }
                                return "\(await jar.drops) drops in the jar"
                            }
                        }
                    }
                }
                HStack {
                    Button("Where is everyone?") {
                        Task {
                            report = await whereIsEveryone(jar)
                            print(report)
                        }
                    }
                    Button("Which queue?") {
                        Task {
                            report = await whoRunsWhat(jar)
                            print(report)
                        }
                    }
                }
                HStack {
                    Button("1,000 bees") {
                        Task {
                            await errand("A thousand bees") { await swarm() }
                        }
                    }
                    Button("10,000 bees") {
                        Task {
                            await errand("Ten thousand bees") { await poolSwarm() }
                        }
                    }
                    Button("Look inside") {
                        Task {
                            await errand("Eve's flight") { await lookInside() }
                        }
                    }
                }
            }
            .buttonStyle(.borderedProminent)
            .tint(Color(red: 0.85, green: 0.55, blue: 0.1))
            .frame(maxHeight: .infinity, alignment: .bottom)
            .padding(.bottom, 24)
            .padding(.horizontal, 20)
        }
        .task {
            while !Task.isCancelled {
                try? await Task.sleep(for: .milliseconds(100))
                hum += 1
            }
        }
    }

    /// One errand: what it brought back, how long it took, and how often the hive hummed meanwhile.
    func errand(_ name: String, _ work: () async throws -> String) async {
        let before = hum, start = Date()
        do {
            let result = try await work()
            let seconds = Date().timeIntervalSince(start).formatted(.number.precision(.fractionLength(1)))
            report = "\(name): \(result), \(seconds) s. The hive hummed \(hum - before) times meanwhile."
        } catch {
            report = "\(name) failed: \(error.localizedDescription)"
        }
        print(report)
    }
}

Run it and tap Which queue?:

Four kinds of code, and the dispatch queue each ran on — screenshot
Four kinds of code, and the dispatch queue each ran onScreenshot · iPhone 17, iOS 27.0
Console
main actor: com.apple.main-thread
@concurrent: com.apple.root.user-initiated-qos.cooperative
the jar: com.apple.root.user-initiated-qos.cooperative
DispatchQueue.global: com.apple.root.user-initiated-qos

Today, on Apple's platforms, yes — at the bottom, as the source of threads. The main actor's executor hands each piece of its tasks to GCD's main queue, com.apple.main-thread, which is why main-actor code always ends up on the main thread. Every other piece goes to a pool that GCD keeps for Swift concurrency alone, the root queue whose label ends in .cooperative: the @concurrent code ran there, and so did the jar. That is the pool of step 9, sized to the cores.

It is not the pool GCD gives you. DispatchQueue.global() ran the block on GCD's own pool — the same label, without .cooperative — and the two differ in one rule. GCD's pool adds threads when its blocks block, on a semaphore or a sync: that is how GCD keeps a queue moving, and how a thousand waiting blocks become a thousand threads. The cooperative pool never adds threads, because it is built for work that never blocks: a task gives its thread back at every await. A GCD block that waited there would hold one of a handful of threads that every task in the app depends on. So there is no public way to put GCD work on it; one of this lesson's edge cases reaches it through a private flag, for looking.

And GCD stops at the thread. Everything above it is the Swift runtime's: a task is not a block, a continuation is not a closure waiting on a queue, and the jar is not a serial queue — it has no queue of its own, it ran on the same shared pool as the @concurrent code, and the runtime keeps each actor's waiting work itself and runs it one piece at a time on whatever thread the pool lends it.

Even the bottom layer is a choice Apple made, not a part of Swift. The language knows only executors: something that can be handed a piece of a task and will run it. The main actor's executor and the pool are Apple's defaults, and on Apple's platforms they are built on GCD because GCD is already there, already managing the threads of every app. Nothing in Swift requires it. Swift runs on platforms that have no GCD, with executors of its own; an actor can be given an executor you write (SE-0392); and since none of your code names a queue — it says await, @concurrent and actor — Apple could build the defaults on something else tomorrow and the app would not change a line. So do not think of Swift concurrency as GCD with new words. The two do not interoperate underneath: they meet only at the edges, and you have used both — a completion handler that resumes a continuation, as the proof of step 2 does, and a block that starts a Task { }.

Step 12

Why @concurrent exists

One question is left from step 4: why is there a word for "run on the pool" at all, when a UIKit developer ran everything on the pool by default and had a word — DispatchQueue.main.async — for the opposite? Because Xcode 27 turned the default round. A new project sets Default Actor Isolation to MainActor and Approachable Concurrency on — you saw both in Build Settings on lesson 1, and an edge case there measured the difference. Under them, a function of yours is on the main actor unless it says otherwise, and even a nonisolated async function runs on its caller's actor. Code is safe by default and single-threaded by default, and leaving the main actor became something you ask for. @concurrent is the asking. Hive turns both settings off, as Trips does since lesson 17: here a plain async function would go to the pool too, so it is not @concurrent that moves countPollen in this project. It is what says so in the code, where it holds whatever the settings are — and it is why every function in Hive says where it runs.

In GCDIn Swift concurrencyWhere the picture breaks
DispatchQueue.mainthe main actorthe main actor is a place to be, not a queue to post to: code is on it, or it is not
DispatchQueue.main.async { }the hop back after an await, which the compiler writesyou never write it; the second half of the function is back on the actor it left
DispatchQueue.global().async { }@concurrentthe same task goes and comes back; GCD started new work and forgot it
a private serial queuean actora block holds its queue to the end; an actor is free at every await
a blocka taska block holds a thread to the end; a task gives it back at every await

Say the three answers aloud, as to an interviewer. Does await block the main thread? No: it suspends the task, and the main thread is free until the result is ready. When do you need @concurrent? When the work computes rather than waits — never for a request. What is the difference between a task and a thread? A task is a small object on the heap that holds paused work — the next piece of code and the frames it needs, its continuation — and it resumes on any free thread; a thread is what runs one piece at a time, and thousands of tasks share a few.

What you built

Guide: what await does, from one cut function to for await, AsyncStream and an actor of your own, each with a program that runs, is in await, in full.

Cheat sheet: the 7 expansions from this lesson are collected, with the rest, on Expansions.

Next, Trips puts all of this to work: every trip's weather at once with a task group, and a cache that is an actor.

The finished project

Stuck, or want to compare with your own? This is the project as it stands at the end of this lesson, built and run before it was put here.

Flash cards

21 cards for this lesson, each labelled with its topic. 17 ask how you would write something; the other 4 are trick questions, written to catch a misunderstanding. Answer in your head first, then tap the card.

Syntax · awaitCall an async function that returns a value, from a button in a view.
Button("Send a bee") {
    Task {
        nectar = await gather()
    }
}
Screenshot of this code running
Screenshot · iPhone 17, iOS 27.0

A button's action is synchronous, so it starts a Task { } and awaits inside it. The task inherits the view's actor — the main actor — so the code after the await may set @State. The await suspends the task, not the thread: the view keeps drawing until gather() returns.

Syntax · @concurrentWrite a function that does two seconds of arithmetic without freezing the user interface.
@concurrent func countPollen() async -> Int {
    var grains = 0
    for i in 0..<25_000_000 where sin(Double(i)) > 0.999 {
        grains += 1
    }
    return grains
}

@concurrent on an async function: it runs on the shared thread pool, and the caller's await leaves the caller's actor free. Everything the body calls must be on no actor too — not @MainActor — or the function hops back to the main actor to call it. On its caller's actor the same loop freezes that actor for two seconds, as a trick card in this lesson shows.

Syntax · TasksStart three visits at once and wait for all three.
async let ada = visit("Ada", for: 300)
async let bea = visit("Bea", for: 300)
async let cia = visit("Cia", for: 300)
return await [ada, bea, cia]

async let starts a child task for each, at once; one await collects them. Three 300 ms visits took under 0.7 s: ["Ada back", "Bea back", "Cia back"] — together. The children are structured: they end with the function, and are cancelled if its task is.

Syntax · ActorsProtect a counter that many tasks change at once, without a lock.
actor Jar {
    private(set) var drops = 0
    func pour() {
        drops += 1
    }
}
…
await jar.pour()

actor in place of class. Its state is touched by one task at a time, and every call into it from outside is an await — a hop to the actor's executor and back. 500 tasks pouring at once left: 500.

Syntax · TasksStop a loop of your own when its task is cancelled.
while !Task.isCancelled {
    n += 1
    await Task.yield()
}

Cancellation is a flag; nothing is stopped from outside. A loop looks at Task.isCancelled — or calls try Task.checkCancellation(), which throws — and Task.sleep does the same for you. The await Task.yield() gives other tasks a turn: a loop with no await in it never suspends.

Syntax · IsolationWrite a function that reports which actor its caller is on, without being told.
func whereAmI(_ isolation: isolated (any Actor)? = #isolation) -> String {
    isolation.map { "\(type(of: $0))" } ?? "no actor"
}

An isolated parameter: the function runs on that actor, and #isolation as its default is filled in at the call site with the caller's own actor. From the main actor and from a @concurrent function it printed: MainActor / no actor.

Syntax · TasksStart work from a view so that it is cancelled when the view goes away.
Text("hum \(hum)")
    .task {
        while !Task.isCancelled {
            try? await Task.sleep(for: .milliseconds(100))
            hum += 1
        }
    }

The .task modifier: a structured task that starts when the view appears and is cancelled when it disappears. It runs on the main actor, like the view, so it may change @State. Task { } in onAppear would start an unstructured task that nothing cancels.

Syntax · TasksRun a piece of work on the shared thread pool as a new task, and wait for its result.
let n = await Task.detached {
    grains()
}.value

Task.detached makes a new task on the pool, on no actor, with none of the caller's priority, cancellation or task-local values; .value awaits its result. It is @concurrent written out by hand — minus the one thing @concurrent keeps: it is the same task, so it sees the caller's cancellation. A detached task does not.

Syntax · TasksWhat is the Task value that Task { } gives back — and how big is it?
let task = Task { 42 }
print(type(of: task), MemoryLayout.size(ofValue: task), await task.value)

A pointer. It printed Task<Int, Never> 8 42: eight bytes, the address of the task object the runtime made on the heap — the object that holds the piece to run next, the frames of the paused functions, the result, the priority and the cancellation flag. Copy the value and you copy the pointer: cancel() or .value on any copy reaches the same task.

Syntax · TasksStart one child task for each of a list of bees, however many there are, and take their results as they finish.
await withTaskGroup(of: String.self) { group in
    for (bee, ms) in bees {
        group.addTask {
            try? await Task.sleep(for: .milliseconds(ms))
            return bee
        }
    }
    for await bee in group {
        back.append(bee)
    }
}

withTaskGroup: addTask starts a child at once, and for await takes the results in the order the children finish, not the order they were added. It printed: added Ada, Bea, Cia; back: Bea, Cia, Ada. The group does not return until every child has finished, and cancelling the task that runs it cancels them all — the structured way to start as many tasks as there are things, where async let is the way for a fixed few.

Trick question · awaitTwo buttons each await something for two seconds: one a Task.sleep, the other a loop of arithmetic in an async function marked @MainActor. Which freezes the user interface, and why?
try? await Task.sleep(for: .seconds(2))
…
_ = await grind()
Screenshot of this code running
Screenshot · iPhone 17, iOS 27.0

The arithmetic. Both are awaits, but await only suspends where something really waits. Task.sleep suspends the task and the main actor is free; the loop never suspends, and it runs on the main actor because the function says so. A counter on the main actor printed: wait: the hive hummed 19 times work: the hive hummed 0 times

Trick question · @concurrentIn a new Xcode project, a @concurrent function calls grind(), a plain synchronous function of the same project, to do its work. Where does the work run?
func grind() -> Int {
…
@concurrent func heavy() async -> Int {
    await grind()
}
Screenshot of this code running
Screenshot · iPhone 17, iOS 27.0

On the main actor — and the interface freezes. grind() is main-actor code, as every function of a new project is by default, so the @concurrent function hops back to the main actor to call it; the await the compiler demands is that hop. It printed: ground on the main thread: true the hive hummed 0 times. @concurrent moves only its own body; the work it calls must be nonisolated to come with it. In Hive, where nothing is on an actor unless it says so, the same grind() belongs to no actor and goes along — and main-actor code says @MainActor, where you can see it.

Trick question · ActorsAn actor method reads a counter, awaits for 10 ms, then writes the counter plus one. 100 tasks call it at once. What is the counter at the end?
func pourSlowly() async {
    let seen = drops
    try? await Task.sleep(for: .milliseconds(10))
    drops = seen + 1
}

About 1, not 100 — it printed 1. The actor let no two tasks touch drops at the same instant, but it is free at every await inside it: all 100 read 0, waited, and wrote 1. Reentrancy. Read, change and write in one stretch of synchronous code, with no await between.

Trick question · TasksA task starts an async let child, a Task.detached child, and calls a @concurrent function. Then the task is cancelled. Which of the three notice?
async let child = nap("async let")
let detached = Task.detached { await nap("detached") }
let look = await lookAround()

The async let child and the @concurrent function; not the detached task. Cancelling t reaches its children, and @concurrent only moved t itself to the pool. Task.detached made a new task with no parent. It printed: async let: cancelled detached: finished @concurrent: sees cancelled = true

Syntax · awaitWhat does the compiler do with an async function at its await?
func flyFirstHalf(then: @escaping @Sendable (Int) -> Void) {
    // everything before the await
    print("Ada leaves from \(whereAmI())")
    // the wait. The closure runs when the data has arrived, on some other thread —
    // so it hops back to the actor the first half was on, and runs the second half there
    URLSession.shared.dataTask(with: meadow) { data, _, _ in
        Task { @MainActor in
            flySecondHalf(data ?? Data(), then: then)
        }
    }.resume()
    // and the first half returns here: the thread is free
}

@MainActor

It cuts the function in two. The first half runs to the await and returns — the thread is free; the second half runs when the result is ready, with the locals handed along, back on the actor the first half was on. Run side by side, the sugar and the two hand-written halves printed: Ada leaves from MainActor Ada is back on MainActor Ada leaves from MainActor Ada is back on MainActor both brought nectar, both back on the main actor

Syntax · @concurrentWhat is @concurrent, written out — and what is the one thing the written-out version gets wrong?
func countPollenByHand() async -> Int {
    // a new task, on the shared pool, on no actor — and .value: suspend here until it is done
    await Task.detached {
        var grains = 0
        for i in 0..<60_000_000 where sin(Double(i)) > 0.999 {
            grains += 1
        }
        print("counted on \(whereAmI())")
        return grains
    }.value
}

await Task.detached { body }.value: the body on the pool, the caller's actor free meanwhile. Both ran on no actor and returned to the main actor: counted on no actor counted on no actor the same grains and back on MainActor. The difference: Task.detached is a new task, so the caller's cancellation never reaches it; @concurrent moves the same task, and sees it.

Syntax · Tasksasync let x = f() — what does that one line do, by itself?
/// A one-second errand that reports whether it was cancelled.
func errand() async -> String {
    do {
        try await Task.sleep(for: .seconds(1))
        return "done"
    } catch {
        print("errand: cancelled")
        return "cancelled"
    }
}

/// The line, then a read of it.
func startThenRead() async -> (lineTook: TimeInterval, readTook: TimeInterval, value: String) {
    let before = Date()
    async let x = errand()
    let lineTook = Date().timeIntervalSince(before)
    let value = await x
    return (lineTook, Date().timeIntervalSince(before) - lineTook, value)
}

/// The line, and never a read: the closing brace cancels the child, then waits for her.
func startAndLeave() async {
    async let _ = errand()
}

It starts f() now, as a child task of the task you are in, and makes x the name of the slot its result will land in. The line does not wait: in the compiler's output it is a slot in your frame, the right-hand side turned into a function of its own, and one startAsyncLetWithLocalBuffer. Reading is another line, await x, which waits only while the slot is empty. The closing brace is where a child nobody read is cancelled and then waited for. The run shows all three: the line costing nothing, the read waiting a second, and startAndLeave returning only after its cancelled errand had ended.

Syntax · ActorsWhat does an actor method with an await inside it expand to?
    func pourSlowlyByHand() async {
        let seen = startPour()                           // job 1, in the jar
        try? await Task.sleep(for: .milliseconds(10))    // not in the jar: anyone may pour now
        finishPour(seen)                                 // job 2, in the jar again
    }

Two jobs for the actor, with the actor free between them — so another task's first job runs there and sees the same value. 100 tasks each reading, waiting and writing: both lost the drops: 1 and 1 of 100. Keep a read-modify-write on one side of an await.

Syntax · IsolationWhat does #isolation expand to at a call site?
func whereAmI(_ isolation: isolated (any Actor)? = #isolation) -> String {
    isolation.map { "\(type(of: $0))" } ?? "no actor"
}

The caller's own actor, as the argument: from main-actor code whereAmI() is whereAmI(MainActor.shared), from a @concurrent function it is whereAmI(nil). Both comparisons printed true true. isolated on the parameter makes the function run on whatever actor it is handed.

Syntax · TasksWhat does Task { } expand to, created from main-actor code?
Task.detached(priority: Task.currentPriority) {
    await MainActor.run {
        print("Task { } by hand: \(whereAmI())")
    }
}

A new task on the pool that first hops to its creator's actor and keeps its creator's priority — which is why a Task { } in a view may touch @State. Without the hop it is Task.detached: Task { }: MainActor Task { } by hand: MainActor Task.detached: no actor. Neither has a parent to be cancelled by; keep the Task value if you need to cancel it.

Syntax · Main actor@concurrent code calls a main-actor function, and the compiler makes you write await. What is that await, written out?
@concurrent func fromThePoolByHand() async {
    await MainActor.run {
        report()
    }
}

A hop to the main actor and back: await MainActor.run { report() }. Both versions ran the report on the main actor: report on MainActor report on MainActor. In GCD this was DispatchQueue.main.async { }, written by hand every time and with no way back.

Edge cases and tricky questions

13 questions of the kind an interviewer uses to find out whether you really know this — the corners, the traps, and the things that changed recently. Each has its source. Wherever the answer is what some code does, that code was compiled and run, and what you see under “Show the answer” is what it really did. Say your answer aloud before you open it.

Lesson 37 · question 1 of 13 · does await block the main thread?

A view on the main actor calls try await URLSession.shared.data(from:) for a real request. While the request is out, does the main thread do anything else?

import SwiftUI

struct ContentView: View {
    @State private var hum = 0
    @State private var bytes = 0

    var body: some View {
        VStack {
            Text("hum \(hum)")
            Text("bytes \(bytes)")
        }
        .task {
            let before = hum
            let url = URL(string: "https://api.open-meteo.com/v1/forecast?latitude=47.6&longitude=-122.3&current=temperature_2m")!
            print("before the request, main thread: \(onMainThread())")
            if let (data, _) = try? await URLSession.shared.data(from: url) {
                bytes = data.count
            }
            print("after the request, main thread: \(onMainThread())")
            print("hummed \(hum - before) times while the request was out")
        }
        .task {
            while !Task.isCancelled {
                try? await Task.sleep(for: .milliseconds(100))
                hum += 1
            }
        }
    }
}

nonisolated func onMainThread() -> Bool {
    Thread.isMainThread
}
Show the answer
Screenshot of this code running
Screenshot · iPhone 17, iOS 27.0

Yes — it is free. A second task on the main actor kept counting while the request was out:

Console
before the request, main thread: true
after the request, main thread: true
hummed 5 times while the request was out

The code before the await and the code after it both ran on the main thread, and in between the main thread ran other work. await suspends the task: the function is cut in two at the await, the first half returns, and the second half runs when the data arrives, back on the main actor. Nothing waits on a thread. This is the whole reason a request on the main actor needs no background queue.

Where this comes from: SE-0296: Async/await · WWDC21: Swift concurrency: Behind the scenes (2021 — the thread model is unchanged)

Lesson 37 · question 2 of 13 · the same await, a computation

The same view awaits an async function marked @MainActor that does two seconds of arithmetic instead of a request. Does the main thread stay free this time?

import SwiftUI

struct ContentView: View {
    @State private var hum = 0

    var body: some View {
        Text("hum \(hum)")
            .task {
                let before = hum
                _ = await heavy()
                print("hummed \(hum - before) times")
            }
            .task {
                while !Task.isCancelled {
                    try? await Task.sleep(for: .milliseconds(100))
                    hum += 1
                }
            }
    }
}

nonisolated func onMainThread() -> Bool {
    Thread.isMainThread
}

/// Two seconds of arithmetic, belonging to no actor.
nonisolated func grind() -> Int {
    var n = 0
    for i in 0..<12_000_000 where sin(Double(i)) > 0.999 { n += 1 }
    print("ground on the main thread: \(onMainThread())")
    return n
}

/// On the main actor: it says so.
@MainActor func heavy() async -> Int {
    grind()
}
Show the answer
Screenshot of this code running
Screenshot · iPhone 17, iOS 27.0

No — it freezes. The counter did not move once:

Console
ground on the main thread: true
hummed 0 times

An await is a place where the task may suspend. A request suspends it, because there is something to wait for; arithmetic never does, so the task runs straight through on whatever actor the function is on — the main actor, as its first word says (and as every function of a new Xcode project is, without the word). The rule: waiting on the main actor is free, computing on it is not. For computing there is @concurrent.

Where this comes from: SE-0296: Async/await · SE-0466: Default actor isolation

Lesson 37 · question 3 of 13 · @concurrent that calls main-actor code

heavy() is marked @concurrent, and does its work by calling grindOnMain(), a function of the same project marked @MainActor. The compiler makes you write await grindOnMain(). Where does the work run?

import SwiftUI

struct ContentView: View {
    @State private var hum = 0

    var body: some View {
        Text("hum \(hum)")
            .task {
                let before = hum
                _ = await heavy()
                print("hummed \(hum - before) times")
            }
            .task {
                while !Task.isCancelled {
                    try? await Task.sleep(for: .milliseconds(100))
                    hum += 1
                }
            }
    }
}

nonisolated func onMainThread() -> Bool {
    Thread.isMainThread
}

/// Two seconds of arithmetic, belonging to no actor.
nonisolated func grind() -> Int {
    var n = 0
    for i in 0..<12_000_000 where sin(Double(i)) > 0.999 { n += 1 }
    print("ground on the main thread: \(onMainThread())")
    return n
}

/// On the main actor: it says so.
@MainActor func grindOnMain() -> Int {
    grind()
}

@concurrent func heavy() async -> Int {
    await grindOnMain()
}
Show the answer
Screenshot of this code running
Screenshot · iPhone 17, iOS 27.0

On the main actor — the @concurrent function hops back to it, and the interface freezes.

Console
ground on the main thread: true
hummed 0 times

That await the compiler demanded is the hop: a call from nonisolated code into main-actor code is a change of actor. @concurrent moves only the body you wrote; everything it calls runs where it belongs. For the work to come along, every function on the way must be on no actor — here, a function with no word; in a new Xcode project, one that says nonisolated — this is why @concurrent is a word for the leaf of the computation, not a wrapper to put round a slow call.

Where this comes from: SE-0461: nonisolated(nonsending) and @concurrent · SE-0466: Default actor isolation

Lesson 37 · question 4 of 13 · nonisolated is not @concurrent

Swift 6.2 made nonisolated async functions nonsending by default. What does that mean for a nonisolated async function that does two seconds of arithmetic, called from the main actor?

import SwiftUI

struct ContentView: View {
    @State private var hum = 0

    var body: some View {
        Text("hum \(hum)")
            .task {
                let before = hum
                _ = await heavy()
                print("hummed \(hum - before) times")
            }
            .task {
                while !Task.isCancelled {
                    try? await Task.sleep(for: .milliseconds(100))
                    hum += 1
                }
            }
    }
}

nonisolated func onMainThread() -> Bool {
    Thread.isMainThread
}

/// Two seconds of arithmetic, belonging to no actor.
nonisolated func grind() -> Int {
    var n = 0
    for i in 0..<12_000_000 where sin(Double(i)) > 0.999 { n += 1 }
    print("ground on the main thread: \(onMainThread())")
    return n
}

/// nonisolated — and, since Swift 6.2, nonsending: it runs on its caller's actor.
nonisolated func heavy() async -> Int {
    grind()
}
Show the answer
Screenshot of this code running
Screenshot · iPhone 17, iOS 27.0

It runs on the main actor, and freezes it.

Console
ground on the main thread: true
hummed 0 times

Before 6.2 a nonisolated async function always hopped to the shared pool, and people relied on that to move work off the main thread. SE-0461 changed it: such a function now runs on whatever actor its caller is on (nonisolated(nonsending)), which makes passing non-Sendable values to it safe and keeps single-actor code on one actor. The pool is now something you ask for, with @concurrent. This is the recent change behind the whole lesson, and Xcode 27 turns it on for a new project through Approachable Concurrency. Hive turns it off, as Trips does: there a nonisolated async function goes to the pool as it did before 6.2 — which is why every function in Hive says where it runs, and one that should follow its caller says nonisolated(nonsending).

Where this comes from: SE-0461: nonisolated(nonsending) and @concurrent · Swift Evolution: Improving the approachability of data-race safety (the vision)

Lesson 37 · question 5 of 13 · a task executor preference

Instead of @concurrent, main-actor code wraps its computation in withTaskExecutorPreference(globalConcurrentExecutor) { … }. Does the work move off the main actor?

import SwiftUI

struct ContentView: View {
    @State private var hum = 0

    var body: some View {
        Text("hum \(hum)")
            .task {
                let before = hum
                _ = await withTaskExecutorPreference(globalConcurrentExecutor) { grind() }
                print("hummed \(hum - before) times")
            }
            .task {
                while !Task.isCancelled {
                    try? await Task.sleep(for: .milliseconds(100))
                    hum += 1
                }
            }
    }
}

nonisolated func onMainThread() -> Bool {
    Thread.isMainThread
}

/// Two seconds of arithmetic, belonging to no actor.
nonisolated func grind() -> Int {
    var n = 0
    for i in 0..<12_000_000 where sin(Double(i)) > 0.999 { n += 1 }
    print("ground on the main thread: \(onMainThread())")
    return n
}
Show the answer
Screenshot of this code running
Screenshot · iPhone 17, iOS 27.0

No. It ran on the main thread and the counter did not move:

Console
ground on the main thread: true
hummed 0 times

A task executor preference says where nonisolated code of the task should run when nothing else decides; code isolated to an actor runs on that actor's own executor regardless, and the closure here inherits its caller's isolation. SE-0417 is for tasks that spend their time in nonisolated code and want a particular thread or pool. To leave the main actor for a computation, mark the computation @concurrent.

Where this comes from: SE-0417: Task executor preference · SE-0461: nonisolated(nonsending) and @concurrent

Lesson 37 · question 6 of 13 · cancellation: who hears it

A task starts an async let child, an unstructured Task { } and a Task.detached { }, each napping for half a second; then the task is cancelled. Which of the three are cancelled?

// it only waits: on its caller's actor
nonisolated(nonsending) func nap(_ who: String) async -> String {
    do {
        try await Task.sleep(for: .milliseconds(500))
        return "\(who): finished"
    }
    catch { return "\(who): cancelled" }
}

let t = Task {
    async let child = nap("async let child")
    let unstructured = Task { await nap("Task { }") }
    let detached = Task.detached { await nap("Task.detached") }
    print(await child); print(await unstructured.value); print(await detached.value)
}
try? await Task.sleep(for: .milliseconds(100))
t.cancel()
await t.value
Show the answer

Only the async let child.

Console
async let child: cancelled
Task { }: finished
Task.detached: finished

async let makes a child task: structured, owned by the task that made it, cancelled with it, and unable to outlive it. Task { } and Task.detached { } make unstructured tasks — nobody's children. Task { } inherits its creator's actor, priority and task-local values; Task.detached inherits nothing; neither inherits cancellation. To cancel an unstructured task you keep the Task value and call cancel() on it yourself — which is why a view that starts one keeps it in a property.

Where this comes from: SE-0304: Structured concurrency · Task

Lesson 37 · question 7 of 13 · an actor and an await

An actor method does drops += 1 twice in a row, with no await between. 500 tasks call it at once. Can the count come out below 1000 — and what if there is a 10 ms await between the read and the write?

actor Jar {
    var drops = 0
    func pour() {
        drops += 1
        drops += 1
    }
    func pourSlowly() async {
        let seen = drops
        try? await Task.sleep(for: .milliseconds(10))
        drops = seen + 1
    }
}
let jar = Jar()
await withTaskGroup(of: Void.self) { g in for _ in 0..<500 { g.addTask { await jar.pour() } } }
print("two increments, no await: \(await jar.drops)")
let slow = Jar()
await withTaskGroup(of: Void.self) { g in for _ in 0..<100 { g.addTask { await slow.pourSlowly() } } }
print("read, await, write: \(await slow.drops) of 100")
Show the answer

Never below 1000 — and with the await, about 1.

Console
two increments, no await: 1000
read, await, write: 1 of 100

An actor runs one task at a time, so two increments with no await between them are one uninterrupted stretch: the count is exact. But an actor is reentrant: at every await inside a method the actor is free, and the next task's call runs. With the read before the await and the write after it, all 100 tasks read 0, waited, and wrote 1. The actor kept its promise — no two touched drops at once — and the method lost 99 drops anyway. Keep a read-modify-write on one side of an await.

Where this comes from: SE-0306: Actors

Lesson 37 · question 8 of 13 · an isolated parameter

A global function has a parameter _ isolation: isolated (any Actor)? = #isolation. Can you also mark the function nonisolated?

nonisolated func whereAmI(_ isolation: isolated (any Actor)? = #isolation) -> String {
    isolation.map { "\(type(of: $0))" } ?? "no actor"
}

print(whereAmI())
Show the answer

It compiles, with a warning

global function with 'isolated' parameter cannot be 'nonisolated'; this is an error in the Swift 6 language mode

No. In the Swift 5 language mode — a new project's — the compiler warns: "global function with 'isolated' parameter cannot be 'nonisolated'; this is an error in the Swift 6 language mode". In the Swift 6 mode the same words are an error: global function with 'isolated' parameter cannot be 'nonisolated'

A function with an isolated parameter is isolated — to whatever actor is passed — so it cannot also be nonisolated. #isolation as the default fills in the caller's actor at the call site, which is how whereAmI() with no argument always answers for its caller.

Where this comes from: SE-0420: Inheritance of actor isolation (isolated parameters, #isolation) · SE-0313: Improved control over actor isolation

Lesson 37 · question 9 of 13 · Task { } and its actor

A view's button starts Task { … }, and inside it a Task.detached { … }. On which actor does each closure run?

func whereAmI(_ isolation: isolated (any Actor)? = #isolation) -> String {
    isolation.map { "\(type(of: $0))" } ?? "no actor"
}
await Task {
    print("Task { }: \(whereAmI())")
    await Task.detached {
        print("Task.detached { }: \(whereAmI())")
    }.value
}.value
Show the answer

Task { } on the main actor; Task.detached on none.

Console
Task { }: MainActor
Task.detached { }: no actor

Task { } inherits the actor of the code that creates it — here the main actor — along with its priority and task-local values, so its body may touch the view's state without a hop. Task.detached starts from nothing: no actor, default priority, no inherited values, no cancellation from a parent. Prefer Task { } in a view, .task where the work belongs to the view's life, and @concurrent on the function that computes.

Where this comes from: Task · SE-0304: Structured concurrency

Lesson 37 · question 10 of 13 · the bridge: a serial queue is not a thread

In GCD, six blocks are submitted to one private serial queue, a third of a second apart. Do they run on one thread? And a queue.sync { } from the main thread — which thread runs its block?

import Foundation

nonisolated(unsafe) var threads: [String] = []

let queue = DispatchQueue(label: "jar")
for _ in 0..<6 {
    queue.async { threads.append(String(describing: Thread.current).split(separator: "number = ")[1].prefix(while: { $0 != "," }).description) }
    Thread.sleep(forTimeInterval: 0.33)
}
queue.sync { }
print("\(Set(threads).count) different thread(s) for 6 blocks: \(threads)")
queue.sync { print("queue.sync from the main thread runs on the main thread: \(Thread.isMainThread)") }
Show the answer

A queue is not a thread. In this run the six blocks happened to land on one pool thread — and the sync block ran on the main thread, the caller's own:

Console
1 different thread(s) for 6 blocks: ["2", "2", "2", "2", "2", "2"]
queue.sync from the main thread runs on the main thread: true

A serial queue promises one block at a time, not one thread: GCD lends it whatever pool thread is free, takes the thread back between blocks, and may lend a different one next time. queue.sync from the main thread runs the block right there, on the main thread, with no queue thread involved. An actor is the same idea, and more so: one job at a time on any free thread, and the thread given back at every await. Neither a queue nor an actor is a thread; a thread is what runs the next piece.

Where this comes from: WWDC21: Swift concurrency: Behind the scenes (2021 — the thread model is unchanged)

Lesson 37 · question 11 of 13 · what Task { } and a hop compile into

In the compiled app, what does Task { } become — and an await that calls into an actor?

actor Jar {
    var drops = 0

    func pour() {
        drops += 1
    }
}

func start(_ jar: Jar) {
    Task {
        await jar.pour()
    }
}

start(Jar())
Show the answer

Two calls into the Swift runtime: swift_task_create and swift_task_switch. In LLVM IR — the compiler's last stage before machine code — Task { } is this:

Every call to swift_task_create in the compiled file

call swiftcc %… @swift_task_create(i64 %…, ptr %…, ptr %…, ptr @"$sxIeAgHr_xs5Error_pIegHrzo_s8SendableRzs5NeverORs_r0_lTRTATu", ptr %…) #16
call swiftcc %… @swift_task_create(i64 %…, ptr %…, ptr %…, ptr @"$sxIeAgHr_xs5Error_pIegHrzo_s8SendableRzs5NeverORs_r0_lTRTA.9Tu", ptr %…) #16

It makes the task and the frame of its first function together, and the task goes to an executor to have its first piece run. The await into the jar is this:

Every call to swift_task_switch

musttail call swifttailcc void @swift_task_switch(ptr swiftasync %…, ptr @"$s6HV_e115startyyAA3JarCFyyYacfU_TY0_", i64 %…, i64 0) #2

swift_task_switch is the hop. Its second argument is the next piece of the closure — $s6HV_e115startyyAA3JarCFyyYacfU_TY0_, which the demangler reads as "(1) suspend resume partial function for closure #1 … in start(_:)": the rest of the function, its continuation. With it comes the executor that must run that piece. If the task is already there, the piece runs at once; otherwise the task is handed to that executor and the call returns, which frees the thread. Every await that changes actor is one of these: the second half of fly() going back to the main actor in step 2, and every call into the jar in step 6.

Where this comes from: WWDC21: Swift concurrency: Behind the scenes (2021 — the thread model is unchanged) · SE-0296: Async/await

Lesson 37 · question 12 of 13 · which locals a waiting task keeps

Eve's closure reads two values before its await, and uses only one of them after it. While she waits, does her task keep both — in a Debug build, and in a Release build?

/// Where Eve leaves a mark she has read. `@MainActor`, like the marks: Eve's task is the main actor's.
@MainActor var lastSeen: UInt64 = 0

let eve = Task {
    let kept = keptMark
    let spent = spentMark
    lastSeen = spent                                // done with it before her wait
    try? await Task.sleep(for: .seconds(1))
    lastSeen = kept                                 // needed after it
}
try? await Task.sleep(for: .milliseconds(300))
print("kept in her task: \(taskMemory(of: eve, contains: keptMark)) · spent in her task: \(taskMemory(of: eve, contains: spentMark))")
await eve.value
Show the answer

A Debug build keeps both; a Release build keeps only the one she needs. Built as Product ▸ Run builds, Debug:

Console
kept in her task: true · spent in her task: true

Built optimized, as an archive for the App Store is:

Console
kept in her task: true · spent in her task: false

A paused function's frame — the data of its continuation — has to hold what a later piece of the function will read. The optimizer works out which values those are, and those are all it must keep in the task; a value used only between two awaits can stay in the piece's registers and on the thread's stack, which the piece gives back when it ends. A Debug build keeps every local in the frame, so that the debugger can show any of them at any await.

One more run shows what must means. Optimized again, but with Eve printing the mark she is done with, print(String(spent, radix: 16)), instead of storing it:

Console
deadbeef0badf00d
kept in her task: true · spent in her task: true

It is in her task after all. String(_:radix:) is generic, so it takes its argument by address, and in an async function the space for such a temporary is in the frame; the optimizer leaves the old value there. So the frame is guaranteed to hold what a later piece needs, and may hold leftovers too. Either way, nothing of Eve's is on a thread while she waits.

Where this comes from: WWDC21: Swift concurrency: Behind the scenes (2021 — the thread model is unchanged)

Lesson 37 · question 13 of 13 · GCD code and the cooperative pool

Code written with GCD wants to run on the pool that Swift concurrency uses. Can it get there?

import Foundation

/// The label of the dispatch queue running this code.
nonisolated func label() -> String {
    String(cString: __dispatch_queue_get_label(nil))
}

/// The C function behind `DispatchQueue.global`. Its header documents one value for `flags`: 0.
@_silgen_name("dispatch_get_global_queue")
nonisolated func globalQueue(_ qos: Int, _ flags: UInt) -> DispatchQueue

let done = DispatchSemaphore(value: 0)
DispatchQueue.global(qos: .userInitiated).async {
    print("DispatchQueue.global(qos: .userInitiated): \(label())")
    done.signal()
}
done.wait()
globalQueue(Int(QOS_CLASS_USER_INITIATED.rawValue), 0x4).async {
    print("the same C function, with the undocumented flag 0x4: \(label())")
    done.signal()
}
done.wait()
Task.detached(priority: .userInitiated) {
    print("Task.detached(priority: .userInitiated): \(label())")
    done.signal()
}
done.wait()
Show the answer

Not through any public door. It printed:

Console
DispatchQueue.global(qos: .userInitiated): com.apple.root.user-initiated-qos
the same C function, with the undocumented flag 0x4: com.apple.root.user-initiated-qos.cooperative
Task.detached(priority: .userInitiated): com.apple.root.user-initiated-qos.cooperative

DispatchQueue.global gives GCD's own pool. Swift concurrency's is a separate root queue, with .cooperative on its label, and the only way to it from GCD is a value of flags that Apple does not document — the one the Swift runtime itself passes. It works today and may not tomorrow, and it would be the wrong pool for GCD code anyway: the cooperative pool never adds threads, so a block that waits on a semaphore there holds one of the handful that every task in the app depends on. Keep GCD work on GCD's pool, and reach the cooperative one the supported way — @concurrent, an actor, a child task.

Where this comes from: WWDC21: Swift concurrency: Behind the scenes (2021 — the thread model is unchanged)

Further reading