-
Notifications
You must be signed in to change notification settings - Fork 0
07 — DispatchWorkItem
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.
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.
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:) { ... }
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.
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
DispatchWorkItemcancellation 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. -
Taskcancellation is cooperative and woven through the entire async ecosystem: canceling a task just flips a flag, and everyawaitpoint downstream — in that task and anything it awaits — can checkTask.isCancelledor let a thrownCancellationErrorpropagate. 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.
A DispatchWorkItem is the order ticket you can still tear up before it's cooked.
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.isCancelledand 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.
What happens if you call .cancel() on a DispatchWorkItem after a chef has already started running it?