Compare a one-shot countdown latch with a reusable cyclic barrier as coordination primitives: who counts, who waits, how many times each can be used, and which one you would pick for an iterative simulation whose workers must all finish step k before any starts step k+1.
answer
- latch: down to zero, terminal, asymmetric roles
- barrier: arrivals up to N, resets, symmetric
- arrive-and-continue vs arrive-and-wait
- barrier action = the only single-threaded moment per round
- one dead party hangs a barrier, not a latch
basics
~20 sA latch has separate roles — workers count down, a different thread waits — and is one-shot: zero is permanent. A barrier is symmetric and reusable: every participant calls await, all block until the Nth arrives, then all resume and the barrier re-arms. An iterative simulation needs the barrier.
solid answer
~50 s**Latch:** a counter fixed at N. `countDown` is arrive-and-continue and is called by the workers; `await` is called by someone else. Once the count hits zero it stays zero, so late waiters pass straight through and the object cannot be reused. **Cyclic barrier:** a fixed party size N and a rendezvous point. Every participant calls the same `await`, which is arrive-**and-wait**: the first N−1 callers block, the Nth trips the barrier and all N are released together. The barrier then resets for the next generation, so the same object serves round after round. Many implementations also run an optional *barrier action* once, on the tripping thread, between releasing and re-arming. For an iterative simulation, the barrier is the right primitive: the loop is `compute(step); barrier.await();` repeated, and the barrier both ends step k and gates step k+1. With latches you would have to allocate two fresh latches per step, which works but is pure garbage-creation and gives you no place to hook the phase-transition action.
code
text · 9 linesbarrier = CyclicBarrier(N, action = {
swap(readGrid, writeGrid) // runs once, on the tripping thread
converged = maxDelta < epsilon // no other worker is running here
})
worker(slice):
while not converged:
computeInto(writeGrid, readGrid, slice)
barrier.await() // ends step k AND gates step k+1go deeper
Give the crisp contrast: latch counts down once and is dead; barrier counts arrivals and re-arms. Say which one an iterative simulation needs and why.
Add the role asymmetry (arrive-and-continue vs arrive-and-wait), the generation counter that makes reuse safe, and the barrier action as the per-round single-threaded hook.
Discuss failure semantics — a barrier's mutual dependency means one dead party hangs N−1, so timed waits and a broken state matter — and name phasers for dynamic party counts.
Position both as low-level building blocks: for fan-out/fan-in prefer a completion abstraction that carries results and errors, and treat repeated global rendezvous as a scalability decision (straggler cost) rather than merely an API choice.
## The two primitives side by side | | Countdown latch | Cyclic barrier | |---|---|---| | Count semantics | Counter set to N, decremented; zero is terminal | Party size N, arrivals counted per generation, resets to zero arrivals | | Who signals | Workers (`countDown`, never blocks) | Everyone (`await`, blocks) | | Who waits | A different thread (`await`) | The same participants | | Roles | Asymmetric — signaller and waiter are distinct | Symmetric — every party plays the same role | | Reuse | One-shot | Cyclic, generation after generation | | Late arriver | Passes immediately once open | Joins the *next* generation and blocks | | Hook point | None | Optional barrier action per generation | ## Arrive-and-continue vs arrive-and-wait This is the conceptual core. A latch splits *announcing an event* from *waiting for it*. A barrier fuses them: your announcement **is** your wait. That single design difference produces every other row in the table. Because a latch's signal is decoupled from waiting, a latch can express relationships a barrier cannot — one thread waiting on work done by threads that never wait themselves, or a thousand threads waiting on a single initialization event. Because a barrier's arrival is coupled to waiting, it can express a *mutual* rendezvous that a latch cannot express twice. ## Why the count directions differ A latch counts **down toward zero** because it models the exhaustion of a known set of pending events. A barrier counts **arrivals up toward N** within a generation, then resets the arrival count and increments a generation number. The generation counter is what makes reuse safe: a thread released from generation 7 that immediately loops around and calls `await` again is recorded against generation 8, never mistaken for a straggler still owed from generation 7. ## The iterative-simulation case Bulk-synchronous workloads — grid/stencil simulations, iterative solvers, parallel PageRank-style computations, lock-step game or physics ticks — all have the shape: ``` loop forever: compute my slice of step k rendezvous with all other workers (someone flips the buffers / checks convergence) ``` The rendezvous is exactly arrive-and-wait, repeated. A cyclic barrier with party size N and a barrier action that swaps the read/write grids and tests the convergence criterion expresses this in two lines inside the worker loop. The barrier action is the piece a latch cannot give you: it runs after all N have arrived and before any is released, so it is the only moment in the cycle when a single thread can safely mutate shared phase state with no other worker touching it. Simulating this with latches means allocating a *pair* of latches per step (a done latch for step k and a start gate for step k+1), publishing the new pair to all workers, and doing so without a race — which is a barrier, badly reimplemented. ## Where the latch still wins - **Fan-out/fan-in with a distinct coordinator**: N tasks that report in once, one thread that aggregates. The workers should not be waiting for one another at all. - **Lifecycle gates**: "the cache is warm", "the config is loaded". The terminal-zero behaviour is a feature — anyone arriving later must not block. - **Unknown or varying waiter count**: any number of threads may await a latch; a barrier's party size is fixed at construction and blocking depends on exactly N arriving. - **Non-participating workers**: with a barrier, a worker that finishes and exits without calling `await` strands everyone else. With a latch it just counts down and leaves. ## Failure behaviour differs sharply A latch's failure mode is a hang confined to the waiters, and only until someone counts down. A barrier's failure mode is worse and mutual: if any one of the N participants dies, hangs, or is cancelled, the other N−1 wait forever, because the barrier only trips on the Nth arrival. That is precisely why barrier implementations add a *broken* state that fails all current and future waiters instead of letting them hang, and why timed barrier waits exist. Latches need no such concept — nothing about a latch is mutual. ## Third option: dynamic-party phasers If the number of participants changes between rounds (workers join a later phase, or drop out on convergence), a fixed-party cyclic barrier is wrong: it will either trip early or never trip. A phaser-style primitive with explicit register/deregister operations plus a monotonically increasing phase number covers that case, and also lets non-participants merely *observe* phase advancement without being counted as parties.
- A worker in a barrier-based simulation finishes its slice early and returns from its loop without calling await. What happens?The barrier never sees its Nth arrival, so every other participant blocks forever at that generation. Barriers are mutual: the party size is a contract every participant must honour on every round. Either the worker must keep arriving until the whole computation ends, or you need a primitive with explicit deregistration, such as a phaser, so the expected party size shrinks when a worker leaves.
- Could you build a cyclic barrier out of latches?Only awkwardly. You need a fresh pair of latches per generation plus a race-free way to publish the next generation's latches to all participants before anyone loops around, which is essentially reimplementing the barrier's generation counter under a lock. Real barriers are built directly on a mutex and a condition variable with an arrival count and a generation number, which is simpler and gives you a natural place to run the per-generation action.
A latch is the finish line of a race — runners cross and keep going, and the timekeeper waits for the last one. A cyclic barrier is a hiking group agreeing to regroup at every trail marker: nobody continues until everybody arrives, and the same rule applies at the next marker.
saying these in an interview costs you the question
- Saying a latch can be reset or reused for the next round.
- Describing barrier await as non-blocking for the last arriver — the last arriver trips the barrier, but it is still a rendezvous point where all are released together.
- Claiming a barrier works when the number of participants varies per round; a fixed-party barrier will trip early or never.
- Thinking the barrier action runs on every participant rather than once per generation on a single thread.
- Ignoring that a dead participant hangs all the others in a barrier, and offering no timeout or broken-state handling.