Tasks, Task Groups and Structured Concurrency
34.1 Structured Concurrency
Child tasks (async let, task groups) are scoped: the parent waits for them, cancellation propagates down, and errors propagate up. No leaked work.
withDiscardingTaskGroup (Swift 5.9) is for fire-and-forget children (servers).
func downloadAll(ids: [Int]) async throws -> [User] {
try await withThrowingTaskGroup(of: User.self) { group in
for id in ids {
group.addTask { try await fetchUser(id: id) }
}
var users: [User] = []
for try await user in group { // results arrive as they complete
users.append(user)
}
return users
}
}
// Limit concurrency (e.g., max 4 at a time)
func process(_ items: [Int]) async {
await withTaskGroup(of: Void.self) { group in
var iterator = items.makeIterator()
for _ in 0..<4 { if let item = iterator.next() { group.addTask { await work(item) } } }
while await group.next() != nil {
if let item = iterator.next() { group.addTask { await work(item) } }
}
}
}34.2 Unstructured Tasks
let task = Task(priority: .userInitiated) { // inherits the actor context and priority
try await fetchUser(id: 1)
}
let user = try await task.value
task.cancel()
Task.detached(priority: .background) { // doesn't inherit the context (use rarely)
await cleanupCaches()
}34.3 Cancellation
Cancellation is cooperative: tasks must check for it. URLSession and Task.sleep honor it automatically.
func longComputation() async throws -> Int {
var total = 0
for i in 0..<1_000_000 {
if i.isMultiple(of: 1000) {
try Task.checkCancellation() // throws CancellationError if cancelled
}
total += i
}
return total
}
if Task.isCancelled { return } // cooperative check
try await withTaskCancellationHandler {
try await download()
} onCancel: {
connection.cancel() // immediate reaction (runs concurrently)
}
try await Task.sleep(for: .seconds(2)) // cancellable sleep
await Task.yield() // let other tasks run34.4 Task-Local Values and Priorities
Priorities: .high/.userInitiated, .medium, .low/.utility, .background. There's priority escalation when high-priority work awaits low-priority tasks.
enum RequestContext {
@TaskLocal static var requestID: String = "none"
}
await RequestContext.$requestID.withValue("req-42") {
await handle() // children see "req-42"
}