Why Async/Await Isn’t Just Prettier GCD
Continuations
When writing asynchronous code using async/await, the most obvious improvements are syntactical ones — being able to read the code top-to-bottom and see the execution order, not dealing with masses of completion handlers or complex error handling, etc.
But the benefits go much deeper on the performance side.
The GCD Model
When GCD was introduced way back with Snow Leopard in 2009, thread pools were the right answer (or so I’m told haha). When a thread is busy or blocked, GCD simply spawns another thread to pick up later work.
In practice, this leads to two significant problems: an explosion in the number of active threads and a related slowdown from having to hop between them. Remember that devices have a fixed number of actual CPU cores — 6 on recent iPhones — so having to time slice between, say, 20 threads to ensure they can all progress doesn’t give you the performance gain you might imagine.
Each time execution shifts between threads, it must perform a context switch, which is a kernel-level operation involving saving all of the current thread’s state and loading and resuming the new thread. On top of this, pulling in the new state pollutes the CPU’s cache (L1, L2), causing further slowdowns by forcing values to be fetched from slower locations.
Enter Continuations
With async/await (and Swift Concurrency more broadly) these problems are avoided by adopting some prior art from C# (among other sources).
Instead of spawning an unbounded number of new threads, the runtime uses a fixed pool of threads roughly equal to the number of CPU cores. And to avoid the slowdown from context switches, it uses a mechanism known as a continuation.
Continuations are essentially portable stack frames — a snapshot of the function’s state and local variables that can be paused and resumed elsewhere. Instead of blocking a thread (which forces a context switch to keep work flowing), the function suspends by saving its state into a continuation and simply returns. The thread is immediately free to run other work — no kernel involvement, no context switch, no cache wipe. As threads are freed up, they can cheaply pick up any available continuations and continue execution.
A Restaurant Analogy
Hopefully this helps visualize what we’ve been talking about.
GCD: If a waiter (thread) is waiting for food (an API call), they just stand at the window doing nothing. If more orders come in, the manager hires more waiters (thread explosion). Soon the kitchen is too crowded to move (context switching).
Async/Await: When the food isn’t ready, the waiter puts a “post-it note” (continuation) on the counter and goes to help another table. The manager never hires more waiters than there are tables.
The Takeaway
Async/await isn’t just nicer syntax than GCD — it’s a fundamentally different concurrency primitive. The readability is a bonus.


Beyond the enhancements to performance covered here, the continuation model also sidesteps many other GCD pitfalls — priority inversion, thread explosion, semaphore deadlocks — not by solving them explicitly but by making the conditions that cause them structurally impossible.