Language and Runtime Concurrency Models: Swift
Apple platforms long used Grand Central Dispatch — queues of closures executed by a system thread pool.
Learning Objectives
Use threads and Grand Central Dispatch, dispatch queues and operation queues.
Use Swift concurrency: tasks, async/await, actors and actor isolation, Sendable, task groups, cancellation, @MainActor, data-race diagnostics and structured concurrency.
Prerequisites
Modules 10 and 13 (actors are introduced here and revisited in Module 18).
Plain-Language Explanation
Apple platforms long used Grand Central Dispatch — queues of closures executed by a system thread pool. Swift 5.5 added language-level concurrency: async/await, structured task groups, and actors, objects whose state can only be touched by one task at a time. Swift 6's strict concurrency checking turns many data races into compile-time errors.
The Swift examples are shown for comparison and were not run — no Swift toolchain on the build machine. They target Swift 5.10/6.
Precise Technical Explanation
Threads and Grand Central Dispatch Thread exists, but GCD (DispatchQueue) is the traditional abstraction: submit closures to queues; a system-managed pool executes them.
Serial queues (one item at a time — a lock replacement), concurrent queues (.global(qos:)), the main queue (UI); async, sync (beware deadlock: sync onto the current serial queue), barriers on concurrent queues, DispatchGroup, DispatchSemaphore, QoS classes.
OperationQueue/Operation: dependencies between operations, cancellation flags, max concurrent count.
Swift concurrency tasks Task { } (unstructured, inherits actor and priority), Task.detached, child tasks via async let and task groups; a cooperative thread pool sized to cores.
async / await Suspension points are marked with await; functions marked async; async let starts a child concurrently.
Actors and actor isolation actor types protect their mutable state: calls from outside are awaited and executed one at a time on the actor (a serial executor); actors are re-entrant at await points (state may change across an await inside an actor method).
A marker protocol for types safe to pass across concurrency domains (value types of Sendable members, actors, immutable classes, @Sendable closures); Swift 6 checks it strictly.
withTaskGroup/withThrowingTaskGroup: dynamic child tasks, all awaited before the group returns; a thrown error cancels the remaining children.
Cooperative: Task.isCancelled, try Task.checkCancellation(), withTaskCancellationHandler; cancelling a parent cancels children.
MainActor and UI safety @MainActor isolates UI code to the main thread; the compiler forces await when calling it from elsewhere.
Swift 6 language mode: complete strict concurrency checking (compile-time); Thread Sanitizer in Xcode for runtime races; Main Thread Checker.
async let and task groups form a task tree with automatic cancellation propagation and awaiting — unstructured Task {} should be used sparingly.
Key Terms
Grand Central Dispatch: queues of closures Executes one item at a time
Type whose state is accessed one task at a time Safe to share across concurrency domains Isolation to the main thread
Other calls may run while an actor method awaits
Diagram or Execution Timeline
structuredSyntax and API Overview
actor Counter { private var value = 0; func increment() -> Int { value += 1; return value } }
let c = Counter()
let n = await c.increment() // hop to the actor, run serially, hop backComplete Code Example
GCD, actors, task groups, cancellation and @MainActor (not run):
import Foundation
actor BankAccount {
private(set) var balance: Int
init(balance: Int) { self.balance = balance }
func withdraw(_ amount: Int) -> Bool { // no await inside: check-then-act is atomic on the actor
guard balance >= amount else { return false }
balance -= amount
return true
}
}
func fetch(_ id: Int) async throws -> String {
try await Task.sleep(nanoseconds: UInt64(50_000_000 * id))
try Task.checkCancellation() // cooperative cancellation point
return "item \(id)"
}
@MainActor func render(_ items: [String]) { print("UI shows \(items.count) items") }
@main
struct Demo {
static func main() async throws {
// GCD: a serial queue as a lock
let queue = DispatchQueue(label: "counter")
var counter = 0
DispatchQueue.concurrentPerform(iterations: 1_000) { _ in queue.sync { counter += 1 } }
print("GCD serial queue counter: \(counter)") // 1000
// actor: two concurrent withdrawals cannot both succeed
let account = BankAccount(balance: 100)
async let w1 = account.withdraw(80)
async let w2 = account.withdr
…Line-by-Line Explanation
concurrentPerform runs iterations in parallel; the serial queue's sync serialises increments.
The actor executes withdraw calls one at a time; because there is no await inside it, the check and the subtraction cannot interleave — one withdrawal of 80 succeeds (compare Module 3's race).
The throwing task group awaits all children; an error would cancel the rest.
render is @MainActor, so calling it requires await (a hop to the main thread).
Cancelling the task makes Task.sleep throw CancellationError (or checkCancellation would).
Output
Not run. By Swift's semantics it prints 1000, approved: 1, balance: 20, UI shows 3 items, task cancelled.
Interleaving or State Trace
Actor mailbox: withdraw(80) #1 runs to completion (balance 20) before withdraw(80) #2 starts and fails its guard — the actor serialises calls.
Correctness and Safety Analysis
Actors remove data races on their state; Sendable checking stops unsafe sharing; but actor re-entrancy means invariants must hold at every await inside actor methods — an await between check and act reintroduces the race.
Common Mistakes and Misconceptions
DispatchQueue.main.sync from the main thread (deadlock).
await between check and act inside actor methods.
Overusing unstructured Task {} and Task.detached.
Blocking calls (semaphores, sleep) inside Swift concurrency's cooperative pool.
Failure Modes and Debugging Steps
Xcode Thread Sanitizer, Main Thread Checker, Swift 6 strict concurrency diagnostics, Instruments "Swift Tasks" and "Swift Actors" templates.
Performance and Scalability Implications
The cooperative pool has one thread per core and must never be blocked; GCD can over-create threads when work blocks ("thread explosion"); actors add a hop per cross-actor call.
Security Considerations
UI and security-sensitive state isolated to actors prevents races in authorization checks; keep credentials in Sendable value types.
Platform and Runtime Caveats
Swift concurrency requires Swift 5.5+ (back-deployed to iOS 13/macOS 10.15); Swift 6 language mode enables complete data-race checking; GCD is available on Linux via swift-corelibs-libdispatch.
Real-World Applications
iOS/macOS apps (SwiftUI with @MainActor view models), server-side Swift (Vapor on SwiftNIO).
Best Practices
Prefer actors and structured task groups over manual locking.
Keep @MainActor for UI; offload heavy work.
Avoid blocking in async contexts; mark shared types Sendable.
Adopt Swift 6 strict checking progressively.
Exercises
E1 · Beginner. Why did only one withdrawal succeed with the actor?
Answer: The actor runs one withdraw at a time and the method contains no await, so the balance check and update are atomic with respect to the other call.
E2 · Intermediate. Show how adding await audit() between the guard and the subtraction reintroduces the race.
Answer: At await audit() the actor may run the second withdraw, which also passes the guard (balance still 100); when both resume, both subtract — balance −60. Re-check state after the await or move the await before the check.
E3 · Advanced challenge. Why does DispatchQueue.main.sync { } deadlock when called on the main thread?
Answer: sync waits until the block has run on the main queue, but the main queue is serial and currently busy running the caller — which is waiting — so the block can never start.
Quiz
? What does an actor guarantee?
+ Its mutable state is accessed by one task at a time. :: Serialised execution.
- Its methods never suspend. :: They may await (re-entrancy).
- It runs on the main thread. :: Only @MainActor does.
- Deadlock freedom in all code. :: Not in general.
? What is Sendable?
+ A marker that a type can safely cross concurrency domains. :: Checked by the compiler.
- A network protocol. :: Unrelated.
- An actor subclass. :: Actors are Sendable, but Sendable is a protocol.
- A GCD queue type. :: Unrelated.
? Which are true? (select all that apply)
+ Task groups await all child tasks before returning. :: Structured.
+ Cancellation in Swift concurrency is cooperative. :: Task.checkCancellation.
- DispatchQueue.main.sync from the main thread is safe. :: It deadlocks.
+ Swift 6 language mode diagnoses many data races at compile time. :: Strict concurrency checking.Summary
Swift spans GCD queues and operation queues and modern Swift concurrency — async/await, structured task groups, actors with isolation, Sendable checking, cooperative cancellation and @MainActor — with Swift 6 making data-race safety a compile-time property.
Further Reading
*The Swift Programming Language*, "Concurrency".
WWDC sessions: "Meet async/await in Swift", "Protect mutable state with Swift actors", "Migrate your app to Swift 6".
Apple: "Dispatch" documentation.
Swift Evolution SE-0296, SE-0306, SE-0302, SE-0337.