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 await points 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 no await.

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
  • 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 to wait() for an async
  • Use Task.sleep, never Thread.sleep. Task.sleep suspends (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

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 / OSLog for 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