-
Notifications
You must be signed in to change notification settings - Fork 0
23 — From GCD to Swift Concurrency
The restaurant hires a new head of operations. Same building, same kitchen, same chefs, same order line, same head chef's counter — but the new operations lead redesigns how every station talks to every other station. Instead of a chef scribbling a ticket and shouting across the pass hoping someone picks it up, every handoff now goes through one clear, enforced protocol: you ask for what you need, you wait your turn without blocking the whole line, and the system tells you — at the moment you write the order, not after the kitchen catches fire — if you're about to hand a dish to a station that isn't allowed to touch it.
Nothing about the kitchen's actual job changes. Orders still come in, still get prepped in parallel, still sometimes need to wait on each other, still sometimes need exclusive access to a shared shelf, still eventually land back at the one counter facing the customer. What changes is who's responsible for getting the handoffs right. Under the old rules, every chef had to personally remember the protocol — hop back to the right counter, don't grab the shared shelf while someone else is mid-write, don't let a rush order get stuck behind a slow one holding the only knife. Under the new rules, the building itself refuses to let you make most of those mistakes.
👉 Same kitchen, same goals, cleaner rules — everything this book taught you already has a name on the other side of that redesign.
Every chapter in this book covered one GCD primitive: a queue, a group, a semaphore, a barrier, a QoS tier, a work item. Swift Concurrency (async/await, structured tasks, actors) isn't a different kitchen — it's a language-level redesign of the same coordination problems, with the compiler enforcing rules that GCD only ever documented and hoped you'd follow.
Here's the direct mapping, concept for concept, chapter for chapter:
| GCD | Swift Concurrency |
|---|---|
DispatchQueue / .async
|
Task |
.sync / await (blocking vs suspending) |
await |
DispatchGroup |
async let / TaskGroup
|
DispatchSemaphore |
actor isolation |
.barrier |
actor isolation |
QoS (.userInitiated, etc.) |
Task priority |
DispatchQueue.main |
@MainActor |
DispatchWorkItem.cancel() |
Task.cancel() / cooperative cancellation |
A few of these are worth spelling out, because the mapping isn't just a rename:
-
.async→Task. Both mean "start this work without blocking the caller." ATaskis the unit of concurrent work, the same role a block submitted to a queue played. -
.sync→awaitis a change in kind, not just name..syncblocks the calling thread dead until the work finishes — the chef stands at the pass doing nothing else.awaitsuspends — the chef steps away and the thread is free to pick up other work while this one waits, then picks back up when the awaited value is ready. Same "wait for this before continuing" intent, radically cheaper mechanism. -
DispatchGroup→async let/TaskGroup. Both express "start several things, then wait until all of them report back" — the same "don't call the head chef until every plate for the table is up" rule from Chapter 10, except the compiler now pairs every start with its corresponding wait, so there's noenter()/leave()bookkeeping to get out of balance. -
DispatchSemaphoreand.barrier→actorisolation. Both a semaphore limiting concurrent access and a barrier serializing writes into a shared queue exist to protect one thing: shared mutable state from being touched by two chefs at once. Anactorprotects that same state by construction — you can't reach its stored properties without going throughawait, so the compiler refuses to compile the race in the first place, instead of you remembering to acquire a semaphore or flag a write.barrierevery single time. -
QoS →
Taskpriority. Both are a hint to the scheduler about how urgently this work should run, not a hard guarantee. -
DispatchQueue.main→@MainActor. Both mean "this work must run on the one thread allowed to touch the UI" — the head chef's counter. The difference is enforcement: forgetting to hop back to.maincompiles fine and fails at runtime (Chapter 21's gotcha #2); forgetting@MainActoron UI-touching code is a compile error. -
DispatchWorkItem.cancel()→Task.cancel(). Both are cooperative, not preemptive — cancelling doesn't yank the chef off the line mid-motion, it flags the ticket and the work has to check for that flag and bail out on its own (Chapter 19).
GCD isn't obsolete after this mapping — it's still what the operating system runs underneath Swift Concurrency's task scheduler, and it remains the right tool for a handful of low-level cases this book covered that don't have a clean high-level replacement, like DispatchSource timers and file monitors. But for the day-to-day work of "start this, wait for that, protect this shared thing, get back to main" — which is most of what application code actually does — that work should now be written with async/await.
GCD (this book) Swift Concurrency
DispatchQueue / .async ───────▶ Task
.sync (blocks the thread) ───────▶ await (suspends, doesn't block)
DispatchGroup ───────▶ async let / TaskGroup
DispatchSemaphore ───────▶ actor isolation
.barrier ───────▶ actor isolation
QoS (.userInitiated, etc.) ───────▶ Task priority
DispatchQueue.main ───────▶ @MainActor
DispatchWorkItem.cancel() ───────▶ Task.cancel() (cooperative)
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
DispatchSource, target queues ───────▶ (no clean equivalent —
GCD still does this)
Two towers, same foundation: everything on the left still runs somewhere underneath everything on the right.
// GCD
DispatchQueue.global().async {
let result = fetch()
DispatchQueue.main.async { display(result) }
}
// Swift Concurrency
Task {
let result = await fetch()
await display(result) // display is @MainActor-isolated
}👉 Same two steps — fetch off the main thread, then hand the result to the one place allowed to show it — but the GCD version needs you to remember which queue you're on at every line, while the Swift Concurrency version reads top to bottom as one straight-line story and lets the compiler catch it if you get the isolation wrong.
This chapter is the vs. Swift Concurrency chapter. The table above isn't a side note appended to one GCD tool — it's the answer for all twenty-two of them at once. Every primitive this book walked through chapter by chapter has a named counterpart on the other side, and in every case the counterpart keeps the same responsibility while moving the bookkeeping from "the programmer remembers" to "the compiler enforces." Nothing you learned was wasted — you now know what problem each Swift Concurrency feature actually solves, because you watched GCD solve the same problem by hand first.
Everything you just learned has a name on the other side of the kitchen.
The mapping isn't perfectly 1:1 in every row, and it's worth saying that plainly rather than papering over it. DispatchSource (Chapter 18) — timers, file monitors, signal handlers — has no direct Swift Concurrency replacement; there's an AsyncStream-based bridge for some of these in newer APIs, but the low-level event-source machinery itself is still GCD. Target queues (Chapter 13) also have no equivalent at all in the structured-concurrency model, because actors and Tasks don't have a notion of "run on this other queue's context" the way a DispatchQueue can target another queue. And a semaphore or a barrier isn't exactly the same shape as actor isolation — a semaphore can gate access from arbitrary threads for arbitrary reasons, while an actor only protects its own stored state, reached only through await. The table above is the right mental map, not a lossless translation.
Pick one gotcha from Chapter 21 (retain cycles, forgetting to hop back to main, or priority inversion) and explain how Swift Concurrency addresses it — or doesn't.
Continue to SwiftConcurrencyExplained →