/*:#localized(key: "WrappingUp")
 # Wrapping up

 These two tools answer two different questions, and knowing which one you're
 asking is most of the battle: a group asks "when is this whole batch done?",
 a semaphore asks "how many may run at once?" Both reward careful bookkeeping
 and punish sloppiness with hangs, crashes, and deadlocks. Keep the balance
 rules below close.

 * A **`DispatchGroup`** tracks a set of tasks and fires when they've all
 finished; a **`DispatchSemaphore`** limits how many touch a resource at once.
 * The two **compose**: use a semaphore to cap how many run at once and a group
 to learn when the whole batch has finished, the very pairing the espresso
 example used.
 * `notify(queue:)` runs a callback when the group empties and **never
 blocks**. Prefer it. `wait()` blocks the calling thread, so keep it off the
 main thread.
 * **Pitfall:** unbalanced `enter`/`leave` either **hangs** (too few leaves) or
 **crashes** (too many); a `wait()` with no matching `signal()` deadlocks
 everyone behind it. `defer { group.leave() }` / `defer { sem.signal() }`
 right after is the safe pattern.
 * **Pitfall:** firing dozens of semaphore-blocked tasks at a global queue can
 starve GCD's thread pool. For heavy throttling, a small dedicated queue (or,
 later, structured concurrency) behaves better.
 * **Pitfall:** a semaphore limits *how many* run at once, not *which* runs
 next: don't rely on the order tasks acquire permits.
 * With a count of 1 a semaphore acts as a lock, but safer, harder-to-forget
 tools exist, and actors will replace this entirely.

 ---
 
 ## Up next

 The next chapter is [Concurrency Hazards](Concurrency%20Hazards.playgroundchapter/Overview): the data race you glimpsed in
 the tally example, plus deadlock and priority inversion, what goes wrong when
 shared state and blocking collide, and how to reason about it.

 [Previous](@previous)  |  page 8 of 8
 */
