A reusable rendezvous point is used across thousands of rounds of a simulation, and every round must swap the read and write grids exactly once. Explain how the rendezvous keeps one round's arrivals from being counted in the next round, and where the per-round swap can run so that no participant ever observes a half-updated state.
answer
- arrival count + generation id, both under one lock
- waiter predicate = "my generation is still current"
- boolean flag alone → cross-round contamination
- action: Nth arriver, after all arrive, before any release
- action is serial critical path; if it throws, round fails
basics
~20 sThe rendezvous tags each round with a generation number; a released thread that loops back is recorded against the next generation, so its arrival can never be mistaken for a late arrival in the old one. The swap runs as the barrier action: once, on the thread that completes the round, after all have arrived and before any is released.
solid answer
~60 sA cyclic barrier keeps two pieces of state under its lock: an **arrival count** for the current round and a **generation** identity. Waiters block on the condition "my generation is still current". The Nth arrival does three things atomically with respect to the lock: run the optional **barrier action**, install a new generation with arrivals reset to zero, and broadcast to wake everyone. Because the waiters test the generation rather than a boolean flag, a thread released from generation *g* that immediately loops around and arrives again is counted in *g+1*. There is no window where a fast thread's next-round arrival is credited to the round it just left — the classic bug in hand-rolled barriers built from a plain counter and a flag. The grid swap belongs in the barrier action precisely because of the ordering: it executes after the last arrival and before the first release, so it is the only genuinely single-threaded moment in the round. No participant can observe the grids mid-swap, and no participant can start round k+1 before it is done.
code
text · 16 lineslock, cond
parties = N; arrived = 0; generation = 0
await():
lock()
g = generation
arrived += 1
if arrived == parties:
action() // exactly once, all others blocked
arrived = 0
generation += 1 // monotonic: never returns to g
cond.broadcast()
else:
while generation == g: // guarded predicate, in a loop
cond.wait()
unlock()go deeper
Say that each round has its own identity so a fast thread's next arrival counts toward the next round, and that the per-round work runs once when the last participant arrives.
Write the generation-based await: count arrivals and generation under one lock, waiters loop on 'my generation is still current', and the Nth arriver runs the action then advances the generation and broadcasts.
Explain why a boolean flag races, why the action needs no extra lock, why a throwing action must fail the round, and that the action is serial time on every round.
Frame the action as the round's serial fraction and a consistency/checkpoint boundary, and know when to generalize to a phase-numbered primitive with dynamic registration instead of a fixed party count.
## The reuse problem A rendezvous that resets for another round has to answer a nasty question: after we release N threads, some of them will finish their next slice of work and arrive again *very* quickly — possibly before the slowest released thread has even woken up from the previous round. How do we avoid counting that early re-arrival as part of the round we are still finishing? A naive implementation shows the bug clearly: ``` // BROKEN await(): lock() arrived += 1 if arrived == N: arrived = 0 tripped = true broadcast() else: while not tripped: cond.wait() tripped = false // who resets it? and when? unlock() ``` With a single boolean, a fast thread can loop back and clear or re-set `tripped` while slow threads from the previous round are still waiting on it — so they either wait forever or are released one round too early. Every hand-rolled barrier that "works on my machine" and hangs in production has some variant of this. ## Generations fix it The standard fix is to make the round *identity* explicit and monotonic: ``` state under one lock: parties = N, arrived = 0, generation = G0, broken = false await(): lock() g = generation // remember MY round arrived += 1 if arrived == parties: action() // once per round, single-threaded arrived = 0 generation = new Generation() // or generation += 1 broadcast() unlock(); return while generation == g and not broken: cond.wait() // predicate is "my round is still current" unlock() if broken: throw BrokenBarrier ``` The waiter's guarded predicate is `generation != g` — not a flag someone must clear. Once the generation advances, it never returns to `g`, so: - **No lost wakeup.** A waiter that checks the predicate before sleeping sees the new generation and never sleeps. - **No spurious release.** A thread waiting on generation *g* is released only when *g* actually completes. - **No cross-round contamination.** A fast thread re-arriving is counted against the new generation, because `arrived` was reset under the same lock hold that installed it. The generation number is also the natural handle for breakage: breaking the barrier marks *this* generation broken, so exactly the parties belonging to the failed round fail, and a reset simply installs a fresh generation. ## Where the per-round action runs The barrier action runs on **the thread that supplies the Nth arrival**, while it still holds the barrier's lock, *after* the arrival count reaches N and *before* the generation is advanced and the waiters are broadcast. That placement gives three guarantees: 1. **Exactly once per round.** Only one thread can be the Nth arriver. 2. **Mutual exclusion by construction.** All other participants are blocked in the barrier at that instant — they have arrived and not been released — so the action has the shared phase state to itself. No extra lock is needed to swap the grids. 3. **Fully ordered.** Everything every participant did in round k happened before the action, and the action happened before any participant begins round k+1. So the swap is invisible to everyone: nobody sees the old grid after release and nobody sees the new grid before it. Typical work for the action: swap read/write buffers, aggregate the round's partial results, evaluate a convergence or termination predicate, advance a simulated clock, emit a progress metric, write a checkpoint. ## Consequences worth stating in an interview - **The action is on the critical path.** It runs while N−1 threads are blocked, so its cost is paid serially every round. A heavyweight action (logging, checkpoint I/O) can dominate a fine-grained simulation; that is Amdahl's serial fraction made concrete. - **The action must not block on the participants.** It cannot wait for anything the blocked parties would have to provide — that is an immediate deadlock. - **If the action throws, the round must fail.** The phase transition was partial; releasing the waiters would run round k+1 over half-swapped state. So a throwing action breaks the barrier and its exception is delivered to the waiters. - **Running the swap in every worker instead is wrong** — it would execute N times — and running it in one designated worker *after* release is also wrong, because the other N−1 have already started round k+1 against the unswapped grids. ## The alternative: a phase number in your own code If you need more than a fixed party count — participants joining or leaving between rounds, or observers that want to know the current round without being counted — a phaser-style primitive generalizes exactly this design: a monotonically increasing phase number, explicit register/deregister to change the party count between phases, and an overridable "on advance" hook that plays the role of the barrier action.
- Why is a single boolean "tripped" flag not enough to make a barrier reusable?Because nothing safely determines when the flag should be cleared. A fast thread released from round k can finish its next slice and re-enter the barrier before the slowest thread of round k has woken, and its interaction with the flag either releases that straggler into the wrong round or leaves it waiting forever. A monotonically increasing generation number removes the question entirely: the waiter's predicate is "my round is no longer current", which can only become true once.
- What is the cost of putting expensive work, such as writing a checkpoint, in the per-round action?It runs serially while all other participants are blocked, so it adds directly to every round's wall-clock time and shows up as a fixed serial fraction that caps speedup no matter how many workers you add. If the work is expensive, either move it off the critical path — hand it to a background writer and only rendezvous on its completion when you actually need durability — or amortize it by checkpointing every k-th round.
Think of a relay of numbered heats. Runners are recorded against the heat number they are in, not just "the current race", so someone who finishes heat 7 and immediately lines up again is entered in heat 8 — never mistaken for a straggler still owed from heat 7.
saying these in an interview costs you the question
- Using one boolean flag instead of a generation counter and claiming the barrier is reusable.
- Saying the per-round action runs on every participant, or on a designated worker after release.
- Believing the action needs its own lock to protect shared state, not realizing all other parties are blocked at that moment.
- Putting blocking work in the action that depends on the blocked participants, creating a deadlock.
- Ignoring that the action is serial time paid on every round and therefore a hard limit on speedup.