Chapter 18 — Debugging and performance
The cooperative thread pool, and the one rule
Swift concurrency runs your tasks on a small, fixed pool of threads — the cooperative thread pool — with roughly one thread per CPU core. Tasks share these threads by suspending at
awaitpoints to let others run. Never block a thread in the cooperative pool. Blocking = occupying a thread without suspending — synchronous I/O,Thread.sleep, waiting on a lock/semaphore, or a long CPU loop with noawait.
flowchart TB
subgraph GOOD__Cooperative__await___ [\"Cooperative (await)\"]
G1["task awaits → frees its thread"] --> G2["other tasks run
on that thread"] end subgraph BAD__Blocking__starvation___ [\"Blocking (starvation)\"] B1["task BLOCKS a thread
(sync I/O / semaphore / sleep)"] --> B2["thread frozen, not freed"] B2 --> B3["all pool threads blocked →
nothing else can run → app hangs 💥"] end
on that thread"] end subgraph BAD__Blocking__starvation___ [\"Blocking (starvation)\"] B1["task BLOCKS a thread
(sync I/O / semaphore / sleep)"] --> B2["thread frozen, not freed"] B2 --> B3["all pool threads blocked →
nothing else can run → app hangs 💥"] end
- Never call blocking/synchronous I/O inside async code. Use the async API (
URLSession.data, async - Never bridge async→sync with a
DispatchSemaphore. Blocking a pool thread towait()for an async - Use
Task.sleep, neverThread.sleep.Task.sleepsuspends (frees the thread);Thread.sleep - Offload long CPU work with an actor or
@concurrent(Chapter 15) so it doesn't monopolize a pool
The tools
- Tasks — how many are alive, their lifetimes, and their state (running / suspended). A number that
- Actor contention — how long tasks spend waiting to get onto an actor. High wait times mean an
- Continuations — leaked continuations (never resumed, Chapter 7) show up here as tasks stuck
A catalog of common problems
Performance principles
flowchart LR
P1["don't block the pool"] --> FAST["fast, responsive
concurrent code"] P2["structured > unstructured"] --> FAST P3["right-size concurrency"] --> FAST P4["batch actor calls"] --> FAST P5["isolate only what needs it"] --> FAST
concurrent code"] P2["structured > unstructured"] --> FAST P3["right-size concurrency"] --> FAST P4["batch actor calls"] --> FAST P5["isolate only what needs it"] --> FAST
Debugging techniques
- Name your tasks.
Task(name: "feed-refresh") { … }gives tasks readable names in Instruments and - Log isolation while learning. During development, printing
Thread.current/ whether you're on the os_signpost/OSLogfor structured, low-overhead logging you can view in Instruments alongside- Reproduce hangs with the debugger's task view. When paused, Xcode can show the current task
What we built in this chapter
- Explained the cooperative thread pool (~one thread per core) and its **one unbreakable rule —
- The tools: Thread Sanitizer (audits the unsafe hatches and interop), the Main Thread Checker,
- A problem catalog mapping symptoms → causes → fixes, headlined by pool starvation and **actor
- Performance principles: prefer structured concurrency, right-size concurrency (window it), batch
- Debugging techniques: name tasks, log isolation while learning,
os_signpost/OSLog, and the
Mental model to take away
- Tasks share a small cooperative pool (~one thread per core) that works only because tasks
- Swift 6 proves no data races; Thread Sanitizer audits the places you used unsafe escape hatches
- The concurrency-specific slowdowns are pool starvation and actor contention — learn their
- Fast concurrent code is structured, right-sized, batched, and isolated only where needed — and