skip to content

Synchronization Primitives

The low-level tools for coordinating threads: mutexes, semaphores, condition variables, read-write locks and spinlocks, each with different ownership, fairness and blocking behaviour. Interviewers ask you to pick the right one for a scenario, which requires knowing what each actually guarantees.

part ofComputer science fundamentalsoverview, primer and where to startread it →
on this pageshow

questions

30

You have one coordinator thread that must not proceed until N independent worker tasks have each finished. Describe how a one-shot countdown latch solves this, and what happens to a thread that waits on a latch whose count has already reached zero.

level: juniorimportance: must knowfreq 60%

answer

  1. counter fixed at construction, only counts down
  2. zero is terminal — late await returns immediately
  3. countDown in the cleanup path, always
  4. latch of 1 = starting gun for all workers
  5. arrive-and-continue, not arrive-and-wait

basics

~20 s

A latch is created with the count N. Each worker decrements it once when done; the coordinator blocks in await until the count reaches zero, then continues. The count never resets, so any wait after zero returns immediately instead of blocking.

solid answer

~50 s

A countdown latch holds one non-negative counter fixed at construction time. Two operations: `countDown` decrements by one and never blocks, and `await` blocks until the counter is zero. For "wait for N tasks", create the latch with N, hand it to the workers, have each worker count down in a finally-style cleanup path so it fires even on failure, and have the coordinator call `await` (ideally with a timeout). When the last worker decrements, every waiter is released at once. The key property is that a latch is **one-shot and terminal**: zero is an absorbing state. A thread that awaits after the count already hit zero returns immediately rather than blocking — which makes the latch usable as a "has initialization finished?" gate for threads that arrive late. If you need the same rendezvous again for a second round, you need a *new* latch (or a reusable cyclic barrier instead).

code

text · 12 lines
text
latch = Latch(N)

worker(i):
    try:
        result[i] = compute(i)
    finally:
        latch.countDown()      // fires even if compute() throws

coordinator:
    if not latch.await(30s):
        fail("only " + (N - latch.count) + " of " + N + " workers finished")
    combine(result)            // all writes to result[] are visible here

go deeper

for a junior

Know the two operations, the wait-for-N-tasks shape, and that the count never goes back up. Say explicitly that awaiting an already-open latch returns immediately.

for a middle

Add the failure-path discipline (count down in a cleanup block, await with a timeout) and the start-gate trick with a latch of one. Be able to contrast arrive-and-continue with a barrier's arrive-and-wait.

for a senior

Talk about the visibility guarantee (work before countDown is visible after await), about turning hangs into diagnosable timeouts, and about when a fixed N is the wrong model because task count is dynamic.

for a principal

Frame it as a choice among completion-signalling mechanisms: a latch is fine for a fixed, known fan-out but does not compose or report partial failure. At scale you usually want a completion abstraction that carries results and errors, with the latch reserved for simple lifecycle gates.

## What a countdown latch is A countdown latch is one of the simplest coordination primitives: a single non-negative integer counter, fixed at construction to some N, plus two operations. - **countDown()** — decrement the counter by one, if it is not already zero. It never blocks. When the decrement takes the counter from one to zero, all threads currently blocked in `await` are released. - **await()** — block until the counter is zero. If it is already zero, return immediately. That is the whole primitive. There is no way to increase the count, and no way to reset it. Zero is an *absorbing* state: once reached, the latch stays open forever. ## The canonical use: waiting for N tasks ``` latch = new Latch(N) for i in 1..N: spawn(worker_i): try: do_work(i) finally: latch.countDown() coordinator: latch.await(timeout) aggregate_results() ``` Two details matter more than they look. **Count down on every exit path.** If a worker throws, returns early, or is cancelled without decrementing, the counter never reaches zero and the coordinator waits forever. The decrement belongs in a cleanup block that runs unconditionally — not at the bottom of the happy path. **Await with a timeout.** An unbounded wait converts one lost decrement into a permanently stuck thread with no diagnostic. A bounded wait lets you log "N of M workers reported in" and fail loudly. ## The mirror-image use: a start gate Because `await` releases *all* waiters simultaneously, a latch initialized to **1** works as a starting gun. Every worker awaits the gate latch; the coordinator does its setup and then counts down once, releasing all workers at nearly the same instant. This is the standard way to reduce startup skew when you want to measure contention: without it, the first worker may finish before the last one has even started. A benchmark harness often uses two latches at once — a start gate of 1 and a done latch of N — which is the clearest illustration that a latch is a directional, one-shot signal rather than a general rendezvous. ## Arrive-and-continue vs arrive-and-wait The defining asymmetry of a latch is that the counting party and the waiting party are different roles. `countDown` is *arrive-and-continue*: a worker signals its arrival and keeps going. `await` is pure waiting: the waiter contributes nothing to the count. A cyclic barrier, by contrast, is *arrive-and-wait* — the same call both registers your arrival and blocks you — so every participant is symmetric. This is why a latch cannot express "all N workers meet here before any continues": each worker would have to count down *and* await, and the last one to count down would be released along with everyone else, which happens to work once, but the latch cannot be used for a second round. ## One-shot and terminal — the consequences 1. **Late waiters do not block.** A latch is therefore a durable record that an event has happened, not an edge-triggered notification you can miss. That makes it the natural primitive for "service is initialized" or "configuration is loaded" gates: components starting up at any later time can await and proceed immediately. 2. **No reuse.** Round two needs a fresh latch. Code that tries to reset a latch is either using the wrong primitive (it wants a cyclic barrier or a phaser) or is about to introduce a race between resetting and awaiting. 3. **Extra decrements are harmless but hide bugs.** Counting down more than N times typically saturates at zero rather than going negative, so a double-decrement bug shows up as the coordinator proceeding *early* — with partial results — rather than as a crash. Treat "latch released but results incomplete" as a symptom of exactly this. ## Ordering guarantees Every reasonable latch implementation guarantees that everything a worker did *before* its `countDown` is visible to a thread that returns from `await`. Without that guarantee the primitive would be useless: the coordinator would be released and then read half-written results. Conceptually, each `countDown` *happens-before* the return of each `await` — the latch is both a control signal and a data-visibility boundary. This is why you do not need any additional synchronization around results that each worker wrote to its own slot before counting down. ## When it is the wrong tool If you need repeated rounds, use a cyclic barrier. If you need to bound *concurrent* access rather than wait for completions, you want a semaphore. If the number of tasks is not known until they start spawning, a fixed N is fragile — a dynamically registering phaser or a completion-counting mechanism fits better.

  • What happens if one worker throws an exception before reaching its countDown call?
    The counter never reaches zero, so the coordinator blocks forever on an unbounded await — a silent hang with no error surfaced. The fix is to put countDown in a cleanup block that runs on every exit path, and to record the failure separately so the coordinator can distinguish "all finished" from "all reported in, some failed". Using a bounded await as a second line of defence turns a hang into a diagnosable timeout.
  • Can you reuse a latch for a second round of the same N tasks?
    No. The count only decreases and zero is terminal, so a second round's awaits return immediately without waiting for anything. You either construct a new latch per round or switch to a cyclic barrier or phaser, which are designed to reset for the next generation. Code that attempts to reset a latch is a strong signal that the wrong primitive was chosen.

A latch is a turnstile that is locked until a fixed number of tickets have been dropped in — and once it unlocks, it stays unlocked. Everyone already queued walks through together, and anyone who shows up an hour later walks straight through without stopping.

saying these in an interview costs you the question

  • Saying you can reset or increment a latch to reuse it for the next round.
  • Putting countDown only on the success path, so a failing worker hangs the coordinator forever.
  • Claiming a thread that awaits after the count reached zero will block until someone counts down again.
  • Confusing a latch with a semaphore — a semaphore's permits go up and down to limit concurrency; a latch only counts down to signal completion.
  • Adding extra locks around results because they "might not be visible", not knowing the latch itself provides the ordering guarantee.

context

open as a page

A consumer thread must wait until a shared queue becomes non-empty before taking an item. Explain the monitor pattern — a mutex paired with a condition variable — and why it is preferred to a loop that sleeps briefly and re-checks the queue.

level: juniorimportance: must knowfreq 58%

basics

~20 s

A monitor is a lock protecting shared state plus a condition variable to wait on. The consumer locks, and while the queue is empty calls wait, which atomically releases the lock and sleeps. A producer adds an item under the lock and signals. Polling wastes CPU and adds latency; waiting costs nothing until woken.

open as a page

What does a mutual-exclusion lock (a mutex) actually guarantee to the code that uses it, and what does it explicitly not guarantee?

level: juniorimportance: must knowfreq 78%

basics

~20 s

At most one thread holds a given mutex, so critical sections guarded by that same lock never overlap, and a holder's writes become visible to the next holder. It promises nothing about acquisition order, fairness, deadlock freedom, or data guarded by another lock.

open as a page

What is a readers-writer lock, how do its shared and exclusive modes interact, and what must be true of a workload for it to beat an ordinary mutex?

level: juniorimportance: must knowfreq 62%

basics

~20 s

It has two modes: many threads may hold it in shared (read) mode at once, but exclusive (write) mode admits one holder and excludes all readers. It only beats a plain mutex when reads greatly outnumber writes and critical sections are long enough to repay its higher bookkeeping cost.

open as a page

What is a semaphore in concurrent programming, and what is the difference between a counting semaphore and a binary semaphore?

level: juniorimportance: must knowfreq 70%

basics

~20 s

A semaphore is a counter of permits with two atomic operations: acquire, which takes a permit and blocks while none are free, and release, which returns one. A counting semaphore starts with N permits; a binary semaphore holds at most one.

open as a page

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.

level: middleimportance: must knowfreq 55%

basics

~20 s

A 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.

open as a page

Why must a thread that blocks on a condition variable re-test its predicate in a loop after waking, rather than assuming the awaited condition holds because it was signalled?

level: middleimportance: must knowfreq 70%

basics

~20 s

Waking means "the state may have changed", not "your condition is true". Another thread can consume the state between the signal and your re-acquiring the lock, one condition may serve several predicates, and spurious wakeups are permitted. So: while (not predicate) wait — never if.

open as a page

How can a writer be starved when a lock has shared and exclusive modes, and what admission policies exist to prevent it?

level: middleimportance: must knowfreq 54%

basics

~20 s

Under reader preference, new readers join while readers are already inside, so if reads arrive faster than they finish the reader count never reaches zero and a waiting writer never runs. Writer-preference blocks arriving readers once a writer is queued; fair or phase-fair policies queue both by arrival and alternate between them.

open as a page

How does a semaphore differ from a mutex, and why is a binary semaphore not a drop-in replacement for a mutex?

level: middleimportance: must knowfreq 66%

basics

~20 s

A mutex has an owner: only the thread that locked it may unlock it, which enables reentrancy, priority inheritance and error detection. A semaphore is an ownerless permit counter, so any thread may release. That makes it a signal, not a lock.

open as a page

What is a spinlock, and how does it differ from a blocking (parking) lock in terms of what a waiting thread actually does and what it costs?

level: middleimportance: must knowfreq 56%

basics

~20 s

A spinlock makes a waiting thread loop on an atomic test until the lock frees, keeping its CPU and never involving the scheduler. A blocking lock parks the thread so the CPU runs something else, paying a context switch on both sleep and wake.

open as a page

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.

level: middleimportance: should knowfreq 32%

basics

~20 s

The 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.

open as a page

When releasing threads blocked on a condition variable, when is waking a single waiter correct and when must you wake all of them? Describe what goes wrong in each direction.

level: middleimportance: should knowfreq 48%

basics

~20 s

Waking one is safe only when every waiter on that condition waits for the same predicate and one unit of state satisfies exactly one waiter. Otherwise wake all: the single wake-up can reach a thread whose predicate is false, which consumes it and leaves the right thread asleep. Waking all is always correct but costs a thundering herd.

open as a page

What is a reentrant (recursive) lock, how does it differ from a non-reentrant one, and what are the arguments against reentrancy?

level: middleimportance: should knowfreq 58%

basics

~20 s

A reentrant lock lets the thread that already holds it acquire it again, tracking a hold count and only freeing the lock when the count returns to zero. A non-reentrant lock self-deadlocks on the second acquire. Reentrancy eases recursion and callbacks but lets code re-enter a critical section while invariants are broken.

open as a page

Sketch how a producer–consumer bounded buffer is built out of semaphores. Which semaphores do you need, what does each one count, and why does the order of the acquire calls matter?

level: middleimportance: should knowfreq 54%

basics

~20 s

Use three: an "empty slots" semaphore starting at capacity, a "filled slots" semaphore starting at zero, and a mutual-exclusion semaphore or lock for the buffer itself. Producers acquire empty then the lock; consumers acquire filled then the lock. Acquiring the lock first deadlocks.

open as a page

Under what conditions does busy-wait spinning outperform parking a waiting thread? Give the break-even reasoning, not just a rule of thumb.

level: middleimportance: should knowfreq 48%

basics

~20 s

Spin when the expected remaining hold time is shorter than the round trip of parking and waking, and when a spare core exists so the holder can still run. Long, unpredictable or blocking critical sections, and oversubscribed CPUs, favour parking.

open as a page

N threads must all reach a shared rendezvous point before any continues, and one participant dies, times out, or is cancelled while the others are already waiting. What should happen to the remaining waiters, and why do rendezvous primitives expose an explicit "broken" state instead of letting them keep waiting?

level: seniorimportance: should knowfreq 38%

basics

~20 s

The rendezvous can never complete, so leaving the others blocked is a permanent, silent hang. The barrier is marked broken and all current and future waiters fail immediately with a distinct error, so failure propagates to every participant instead of one loud error plus N−1 stuck threads.

open as a page

A worker occasionally blocks forever waiting for data that was, in fact, produced. Explain the lost-wakeup bug on a condition variable and the exact locking protocol between the producer and the waiter that prevents it.

level: seniorimportance: should knowfreq 45%

basics

~20 s

A signal is not stored: if it fires when nobody is in the wait set, it vanishes. If the producer changes state and signals in the window after the waiter tests its predicate but before it is registered as waiting, the waiter sleeps forever. Prevention: test the predicate and wait under the same mutex that the producer holds while changing state and signalling.

open as a page

What does it mean for a lock to be fair, how does an unfair (barging) lock differ, and when is fairness worth its cost?

level: seniorimportance: should knowfreq 44%

basics

~20 s

A fair lock grants ownership in arrival order via a queue; an unfair lock lets a running thread barge in and take a just-released lock ahead of queued waiters. Barging gives much higher throughput because it avoids wake-up handoff gaps, at the price of a long starvation tail. Fairness pays when hold times are long and tail latency matters.

open as a page

What do a non-blocking lock attempt (tryLock) and a timed lock acquisition give you that a plain blocking acquire does not, and what goes wrong when they are used carelessly?

level: seniorimportance: should knowfreq 52%

basics

~20 s

They bound how long you wait: the attempt returns success or failure instead of blocking indefinitely, so you can take a fallback path, stay responsive, or release locks you already hold and retry. The risks are livelock from lockstep retries and code that ignores the failure branch.

open as a page

How does an optimistic read with validation (the sequence-lock or seqlock idea) work, and what constraints does it place on the reader's code?

level: seniorimportance: should knowfreq 36%

basics

~20 s

The reader takes no lock: it records a version counter, reads the data, then re-reads the counter. If it changed, or was odd (a write in progress), the read is discarded and retried. Readers must therefore tolerate momentarily inconsistent data — no side effects, no dereferencing possibly-freed pointers, no unbounded work before validation.

open as a page

Why is upgrading a shared (read) lock hold to exclusive (write) mode a deadlock hazard, why is downgrading the other way safe, and how do you restructure code that seems to need an upgrade?

level: seniorimportance: should knowfreq 44%

basics

~20 s

If two readers both try to upgrade, each waits for the other to release its shared hold, so neither can ever get exclusive mode — a deadlock with no cycle of distinct locks. Downgrading is safe because the holder already excludes everyone and simply relaxes. Restructure by releasing, re-acquiring exclusively, and re-validating, or by using a single-holder upgradeable mode.

open as a page

A service uses a counting semaphore to cap how many requests are in flight to a downstream dependency at any moment. What failure modes would you design against in that limiter?

level: seniorimportance: should knowfreq 47%

basics

~20 s

Permit leaks on error paths (always release in a finally/defer), double releases that silently raise the cap, unbounded waiting instead of a timeout plus rejection, an unbounded queue of waiters hiding the overload, starvation under barging, and forgetting the cap is per instance, not global.

open as a page

Why do real spin loops read the lock word before attempting an atomic write, and why do they add backoff and adaptive spin limits instead of retrying as fast as possible?

level: seniorimportance: should knowfreq 40%

basics

~20 s

An atomic write takes the cache line exclusively, so hammering it makes every spinner steal the line from the holder and each other. Reading first (test-and-test-and-set) spins on a shared copy; backoff and adaptive limits cut the remaining traffic and stop pointless spinning.

open as a page

Explain the futex-style design used by modern mutexes: why an uncontended acquire needs no call into the operating system, and what has to happen once the lock is contended.

level: seniorimportance: should knowfreq 38%

basics

~20 s

The lock state is an ordinary word in user memory, so an uncontended acquire is one atomic compare-and-swap — a few nanoseconds, no kernel. Only when a thread must actually wait does it make a wait system call naming that address; the kernel queues it and the releaser wakes it.

open as a page

How do you decide how coarse or fine-grained your locking should be, what is lock striping, and what is the convoy effect that can appear under a hot lock?

level: principalimportance: should knowfreq 46%

basics

~20 s

Coarse locking is one lock for a whole structure: simple, but it serializes everything. Fine-grained locking splits protection into independent locks — striping hashes keys onto N locks — raising parallelism but adding multi-lock complexity and deadlock risk. A convoy forms when a holder is descheduled or blocks while holding, waiters pile up, and threads then advance in lockstep, collapsing throughput.

open as a page

You are told a data structure is read-mostly and should therefore be guarded by a shared/exclusive lock. How would you decide whether that is actually the right mechanism, and what alternatives would you weigh?

level: principalimportance: should knowfreq 42%

basics

~20 s

Measure first: read/write ratio, critical-section length, and core count. Mode separation only pays if reads dominate and sections are long enough to amortise the reader bookkeeping. Otherwise weigh a plain mutex, an immutable snapshot swapped atomically, sharded state, per-thread state with aggregation, or optimistic validated reads.

open as a page

You must choose the permit count for a semaphore that bounds in-flight work against a shared dependency. Walk through the reasoning you would use to derive a number rather than guess one, and how you would revisit it as conditions change.

level: principalimportance: should knowfreq 40%

basics

~20 s

Start from Little's law: in-flight = throughput x latency. Pick the throughput you must sustain, multiply by measured service time, then check the number against the dependency's own capacity budget divided across replicas, and keep utilisation below saturation. Then measure and adjust.

open as a page

A service that uses busy-wait spinning performs well on dedicated hardware but collapses when moved onto shared or virtualised machines. Explain the mechanism behind that collapse and what mitigations exist.

level: principalimportance: should knowfreq 34%

basics

~20 s

Spinning assumes the lock holder is running in parallel. Under oversubscription the holder gets preempted, so spinners burn whole scheduling quanta waiting for a thread that cannot run — wasting the very CPU it needs. Fix by bounding spins and parking, or by not oversubscribing.

open as a page

Condition-variable designs differ in what happens at the moment of signalling: under Hoare (signal-and-wait) semantics the signaller immediately yields the lock and the CPU to the woken thread, while under Mesa (signal-and-continue) semantics the signaller keeps running and the woken thread must re-acquire the lock later. Explain how each choice affects the waiter's code, and whether signalling before or after releasing the mutex matters.

level: seniorimportance: nice to knowfreq 26%

basics

~30 s

Under Hoare semantics the woken thread runs with the predicate still guaranteed true, so a single if would suffice — but it needs an extra context switch and lock handoff. Mesa semantics let the signaller continue, so the predicate can be falsified before the waiter runs, forcing a while loop. Every mainstream platform is Mesa. Signalling inside versus just after the critical section is a performance choice, not a correctness one — provided the state change happened under the lock.

open as a page

You are designing a parallel workload in which every worker must synchronize at a global rendezvous at the end of each step before any worker starts the next step. What costs does that repeated global rendezvous impose as the number of workers and the variance in per-worker time grow, and when would you restructure the computation to avoid it?

level: principalimportance: nice to knowfreq 30%

basics

~20 s

Each step costs the slowest worker, not the average, so per-step time tracks the maximum of N samples and grows as N and variance grow. The rendezvous itself is serial work, capping speedup by Amdahl's law. Restructure toward pipelining, work-stealing, or asynchronous/relaxed-consistency iteration when stragglers dominate.

open as a page