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 run

34.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"
}