await really doesTasks, 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.
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.
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.
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 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:
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.
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:
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¤t=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:
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 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:
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.
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)
The second errand has no request in it: counting every grain of pollen in the meadow, a long computation. 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:
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.

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


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.
@concurrent: the same count, on the poolReplace countPollen in Bees.swift with this version. The body is identical; the only change is the first word:
/// 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:



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:
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.
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
Ada flew alone. Send three bees at once, and bring them home with one word. 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:
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: 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:

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:
// 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.
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
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:
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:
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:

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.
awaitNow a slow pour. Add to the bottom of 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:
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:

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:
// 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.
isolated and #isolationEvery 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:
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:
/// 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:
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:

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:
// #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.
Every errand so far was one task, or three. Tasks are meant to be had by the thousand. 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?:
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: 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 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.
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 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:
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:
/// 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:
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:

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.
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.
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:
/// 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?:
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?:

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-qosToday, 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 { }.
@concurrent existsOne 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 GCD | In Swift concurrency | Where the picture breaks |
|---|---|---|
DispatchQueue.main | the main actor | the 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 writes | you never write it; the second half of the function is back on the actor it left |
DispatchQueue.global().async { } | @concurrent | the same task goes and comes back; GCD started new work and forgot it |
| a private serial queue | an actor | a block holds its queue to the end; an actor is free at every await |
| a block | a task | a 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.
@concurrent runs on the pool and leaves it free.async let and called home with one cancel() — structured tasks — and the trip itself, an unstructured Task the view keeps.await.whereAmI(): isolated and #isolation, and each piece of syntax written out by hand — await, @concurrent, async let, an actor method cut at its await, and #isolation — proven to do what the sugar does.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.
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.
Hive.xcodeproj in Xcode.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.
async function that returns a value, from a button in a view.
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.
@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.
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.
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.
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.
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.
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.
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.
Task value that Task { } gives back — and how big is it?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.
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.
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?
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
@concurrent function calls grind(), a plain synchronous function of the same project, to do its work. Where does the work run?
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.
awaits for 10 ms, then writes the counter plus one. 100 tasks call it at once. What is the counter at the end?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.
async let child, a Task.detached child, and calls a @concurrent function. Then the task is cancelled. Which of the three notice?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
async function at its await?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
@concurrent, written out — and what is the one thing the written-out version gets wrong?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.
async let x = f() — what does that one line do, by itself?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.
await inside it expand to?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.
#isolation expand to at a call site?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.
Task { } expand to, created from main-actor code?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.
@concurrent code calls a main-actor function, and the compiler makes you write await. What is that await, written out?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.
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.
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?

Yes — it is free. A second task on the main actor kept counting while the request was out:
before the request, main thread: true
after the request, main thread: true
hummed 5 times while the request was outThe 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)
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?

No — it freezes. The counter did not move once:
ground on the main thread: true
hummed 0 timesAn 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
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?

On the main actor — the @concurrent function hops back to it, and the interface freezes.
ground on the main thread: true
hummed 0 timesThat 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
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?

It runs on the main actor, and freezes it.
ground on the main thread: true
hummed 0 timesBefore 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)
Instead of @concurrent, main-actor code wraps its computation in withTaskExecutorPreference(globalConcurrentExecutor) { … }. Does the work move off the main actor?

No. It ran on the main thread and the counter did not move:
ground on the main thread: true
hummed 0 timesA 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
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?
Only the async let child.
async let child: cancelled
Task { }: finished
Task.detached: finishedasync 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
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?
Never below 1000 — and with the await, about 1.
two increments, no await: 1000
read, await, write: 1 of 100An 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
A global function has a parameter _ isolation: isolated (any Actor)? = #isolation. Can you also mark the function nonisolated?
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
A view's button starts Task { … }, and inside it a Task.detached { … }. On which actor does each closure run?
Task { } on the main actor; Task.detached on none.
Task { }: MainActor
Task.detached { }: no actorTask { } 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
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?
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:
1 different thread(s) for 6 blocks: ["2", "2", "2", "2", "2", "2"]
queue.sync from the main thread runs on the main thread: trueA 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)
In the compiled app, what does Task { } become — and an await that calls into an actor?
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
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
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
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?
A Debug build keeps both; a Release build keeps only the one she needs. Built as Product ▸ Run builds, Debug:
kept in her task: true · spent in her task: trueBuilt optimized, as an archive for the App Store is:
kept in her task: true · spent in her task: falseA 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:
deadbeef0badf00d
kept in her task: true · spent in her task: trueIt 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)
Code written with GCD wants to run on the pool that Swift concurrency uses. Can it get there?
Not through any public door. It printed:
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.cooperativeDispatchQueue.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)