Skip to content

23 — From GCD to Swift Concurrency

rebeloper edited this page Jul 12, 2026 · 1 revision

🍽️ Intuition

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.


🧠 What It Means In Swift

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:

  • .asyncTask. Both mean "start this work without blocking the caller." A Task is the unit of concurrent work, the same role a block submitted to a queue played.
  • .syncawait is a change in kind, not just name. .sync blocks the calling thread dead until the work finishes — the chef stands at the pass doing nothing else. await suspends — 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.
  • DispatchGroupasync 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 no enter()/leave() bookkeeping to get out of balance.
  • DispatchSemaphore and .barrieractor isolation. 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. An actor protects that same state by construction — you can't reach its stored properties without going through await, so the compiler refuses to compile the race in the first place, instead of you remembering to acquire a semaphore or flag a write .barrier every single time.
  • QoS → Task priority. 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 .main compiles fine and fails at runtime (Chapter 21's gotcha #2); forgetting @MainActor on 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.


📊 ASCII Diagram

        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.


💻 Tiny Swift Example

// 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.


🔄 vs. Swift Concurrency

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.


🧸 Memory Sentence

Everything you just learned has a name on the other side of the kitchen.


⚠️ Analogy Limit

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.


✅ Check Your Understanding

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

Clone this wiki locally