Skip to content

07 — DispatchWorkItem

rebeloper edited this page Jul 12, 2026 · 1 revision

🍽️ Intuition

Most orders in this kitchen are shouted straight to a chef: "table 4 wants a burger," and it's already on the grill before you've finished the sentence. That works fine — until you need to do something with the order besides just cook it.

What if the customer changes their mind before the chef has even started? What if the front-of-house wants to know the exact moment the dish is done, so they can call the table? What if you want to hand the same order to a different chef depending on how busy the kitchen is?

Shouting doesn't let you do any of that. Once it's out of your mouth, it's out of your hands.

So instead, you write the order on a ticket. The ticket sits there as a real, physical thing before any chef touches it:

  • It can be handed off to whichever chef is free, instead of committing to one on the spot.

  • It can be torn up before a chef picks it up — the order never happens.

  • Someone can watch the ticket rail and get notified the moment it's marked done.

  • An order shouted directly at a chef → a plain block submitted with .async

  • An order written on a ticket first → a DispatchWorkItem

👉 The work is identical either way — cook the burger. What changes is that the ticket gives you a handle on the order before, during, and after it's cooked.


🧠 What It Means In Swift

A DispatchWorkItem wraps a block of work in an object — the ticket — instead of submitting the block directly. That object is reusable and, crucially, gives you a handle on the work:

  • Create one with DispatchWorkItem { ... }, wrapping whatever code the order needs to run.
  • Submit it to a queue with queue.async(execute: item) — same queue mechanism from Chapter 5, just handing over a ticket instead of a bare closure.
  • Call item.cancel() to tear up the ticket before a chef has picked it up. Once a chef is already cooking, tearing up the ticket does nothing to stop them.
  • Call item.notify(queue:execute:) to register a second block that runs the moment the first one finishes — the ticket rail, watched for completion.
let item = DispatchWorkItem {
    // the order itself
}

queue.async(execute: item)

👉 A plain queue.async { ... } block is a fire-and-forget shout. A DispatchWorkItem is that same block, but written down first — which is what makes canceling it or watching it finish even possible.


📊 ASCII Diagram

ORDER TICKET LIFECYCLE 🧾

  📝 Write the ticket           🧾 DispatchWorkItem { ... }
        │
        ▼
  📌 Hand it to the queue       queue.async(execute: item)
        │
        │   ✂️ item.cancel() — only works HERE, before a chef picks it up
        │
        ▼
  🧑‍🍳 Chef picks up the ticket   (cancel() no longer has any effect)
        │
        ▼
  🍔 Order is cooked
        │
        ▼
  🔔 Ticket marked done          item.notify(queue:) { ... }

💻 Tiny Swift Example

let item = DispatchWorkItem {
    print("Cooking order 🍔")
}
queue.async(execute: item)

item.notify(queue: .main) {
    print("Order done, notify the customer 🔔")
}

// item.cancel() // only works if it hasn't started yet

👉 The ticket is written (DispatchWorkItem), handed to the queue (async(execute:)), and watched for completion (notify(queue:execute:)) — all three steps you'd get for free by shouting the order, except shouting gives you none of them. If item.cancel() had been called before a chef picked up the ticket, neither print would ever run, and notify still fires once cancellation has settled the item's fate.


🔄 vs. Swift Concurrency

The closer analogue on the Swift Concurrency side isn't a block wrapper at all — it's the handle you get back from starting a Task:

let task = Task {
    // the order itself
}

task.cancel()
  • Both give you a handle to something that's running, and both expose .cancel().
  • But DispatchWorkItem cancellation is a blunt, all-or-nothing check: has a chef picked up the ticket yet, or not? Once they have, .cancel() is a no-op.
  • Task cancellation is cooperative and woven through the entire async ecosystem: canceling a task just flips a flag, and every await point downstream — in that task and anything it awaits — can check Task.isCancelled or let a thrown CancellationError propagate. Long-running async work can react mid-flight, not just before it starts.

👉 DispatchWorkItem.cancel() asks "did the ticket even reach a chef yet?" Task.cancel() asks a running chain of work to check in and bail out gracefully, at any point along the way.


🧸 Memory Sentence

A DispatchWorkItem is the order ticket you can still tear up before it's cooked.


⚠️ Analogy Limit

Tearing up a physical ticket is total — once it's shredded, there's no dish, no matter how far along a chef thought they were.

DispatchWorkItem.cancel() isn't nearly that powerful:

  • It only prevents work that hasn't started from starting. If a chef has already picked up the ticket and started cooking, .cancel() does nothing to stop them — the burger keeps cooking to completion.
  • GCD gives you no built-in way to interrupt work that's already mid-execution. If you need that, the block itself has to periodically check item.isCancelled and choose to stop early — GCD won't do it for you.
  • notify(queue:execute:) still fires after a canceled-but-never-started item settles, so "canceled" is a real, observable outcome — just not a way to yank a dish off an active grill.

So "tear up the ticket" is accurate for the narrow window before a chef touches it. Past that window, the ticket is just paper — the cooking is already happening, unaffected by what you do with it.


✅ Check Your Understanding

What happens if you call .cancel() on a DispatchWorkItem after a chef has already started running it?

Clone this wiki locally