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.
answer
- Hoare = signal-and-wait, hands over lock+CPU, if would suffice
- Mesa = signal-and-continue, predicate may go stale, while required
- barging: a never-waited thread can win the lock
- all mainstream platforms are Mesa
- signal inside vs after unlock = scheduling, not correctness
basics
~30 sUnder 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.
solid answer
~1 min**Hoare (signal-and-wait):** signalling atomically transfers the mutex and the CPU to one woken waiter; the signaller is suspended on an "urgent" queue and resumes when the waiter releases. Because nothing runs between the signal and the waiter's resumption, the predicate is still true — `if` would be sound. The price is two extra context switches per signal, a lock handoff, and a signaller whose control flow can be suspended in the middle of a critical section, which makes reasoning about its own invariants harder. **Mesa (signal-and-continue):** signalling merely marks a waiter runnable; the signaller keeps the lock and finishes its critical section. The waiter must re-acquire the mutex, and by then the signaller — or a barging thread that never waited — may have falsified the predicate. Hence the mandatory `while` loop. This is what essentially every real platform implements, because it is much cheaper and lets the signaller finish coherently. **Signal inside or after the unlock?** Both are correct as long as the *state change* happened under the mutex. Signalling inside is simplest and can wake a thread straight into a held lock; signalling after release avoids that hop but widens the window for barging. Treat it as a micro-optimization to measure, not a rule.
code
text · 11 lines// Hoare (signal-and-wait): predicate still true on resume
lock(m)
if not predicate(): cond.wait(m) // 'if' is SOUND here
act()
unlock(m)
// Mesa (signal-and-continue): predicate may have gone stale
lock(m)
while not predicate(): cond.wait(m) // 'while' is MANDATORY
act()
unlock(m)go deeper
It is enough to know that real platforms do not hand the lock to the woken thread, so the predicate can go stale and the waiter must re-check in a loop.
Contrast the two semantics explicitly and connect them to why while is mandatory in practice, plus what barging is.
Explain the costs that made Mesa the industry choice, why the signaller being suspended mid-critical-section is awkward under Hoare, and treat signal placement as a measurable trade-off rather than dogma.
Use the semantics to reason about fairness and tail latency: barging is a deliberate throughput-for-fairness trade, so bounded waiting must be engineered explicitly, and wake-up policy should be treated as tunable design surface rather than an implementation detail.
## Two answers to one question The question a monitor design must answer is: *at the instant a thread signals a condition, who holds the lock next?* Historically there are two answers, and they lead to visibly different waiter code. ## Hoare semantics — signal-and-wait (signal-and-urgent-wait) Signalling is an immediate, atomic handoff: 1. The signaller is suspended and placed on an **urgent** queue. 2. One waiter is woken and given the mutex directly. 3. When that waiter leaves the monitor (or itself waits), threads on the urgent queue get priority over threads merely trying to enter. Because nothing else can execute between the signal and the waiter's resumption, the predicate that motivated the signal is **still true** when the waiter resumes. Consequences: - `if (!predicate) wait();` is sufficient — the waiter needs no re-test. - Reasoning is clean: signalling is an assertion the receiver can trust. - **Cost:** at minimum two extra context switches per signal, plus a lock handoff that defeats fast uncontended lock paths. - **Awkwardness:** the signaller is suspended mid-critical-section, so its own invariants must hold at the point of signalling — a real burden if the signal sits in the middle of a multi-step state update. In practice signals get pushed to the end of the critical section anyway, which erodes the benefit. - The urgent queue's priority rule is extra machinery, and nesting monitors makes it harder still. ## Mesa semantics — signal-and-continue Signalling is a *hint*: 1. One (or all) waiters are moved from the condition's wait set to the mutex's entry queue. 2. The signaller keeps the mutex and continues to the end of its critical section. 3. The woken waiter competes for the mutex like anyone else. Between the signal and the waiter's re-acquisition, arbitrary work can happen: the signaller continues; other threads that were never waiting can acquire the lock first (**barging**); another woken waiter can consume the state. So the predicate may be false when the waiter resumes. Consequences: - `while (!predicate) wait();` is **mandatory**. - Signals become cheap: no handoff, no forced context switch, and the fast uncontended mutex path is preserved. - Implementations may also allow **spurious** wakeups, which cost nothing extra because the loop is already required. - Barging is a deliberate performance choice: allowing a running thread to take a briefly-free lock avoids a context switch, at the cost of fairness. There is a third historical variant, **Brinch Hansen** semantics, which requires the signal to be the *last* action before leaving the monitor — so no ambiguity arises at all. It is the easiest to reason about and the most restrictive to program against; it survives mostly in language designs rather than general-purpose libraries. ## Which one do real systems use? Essentially all modern condition-variable implementations — OS-level and language-level — are Mesa-style signal-and-continue, possibly with spurious wakeups. That is *why* "always wait in a loop" is universal advice: the advice is a direct consequence of the semantics the industry chose. Knowing the Hoare alternative is what lets you explain *why* the loop is required rather than reciting it, and it correctly predicts that under Hoare semantics the loop would be optional. A practical corollary: portable code must be written for the weakest guarantee. Even on a hypothetical no-spurious-wakeup platform, barging alone still forces the loop. ## Signal inside or outside the critical section? A separate question, often confused with the above. ``` // A: signal while holding the mutex lock(m); state.change(); cond.signal(); unlock(m) // B: signal just after releasing lock(m); state.change(); unlock(m); cond.signal() ``` Both are **correct**, provided the state change itself happened under the mutex — that is the part that closes the lost-wakeup race. The difference is scheduling: - **A** may wake a thread that immediately blocks on the mutex the signaller still holds (a "hurry up and wait" hop). Many implementations optimize this away by moving the waiter directly to the mutex queue instead of making it runnable. - **B** avoids that hop, but widens the window in which a barging thread can take the lock and consume the state before the woken waiter arrives — more wasted wake-ups, and worse fairness. What is **not** optional: mutating the predicate's state outside the mutex. Do that and no placement of the signal can save you, because a waiter can evaluate the predicate as false and enter the wait set in the gap. Also note: some platforms *require* holding the lock to signal at all; others merely allow it. Portable code that signals after unlocking must confirm the platform permits it. ## What this buys you as a designer - It explains the loop from first principles instead of as a rule. - It predicts barging and therefore that fairness is not free: if you need bounded waiting, build it (ticketing, per-thread conditions, explicit handoff) rather than expecting the primitive to provide it. - It clarifies that priority and wake-up order are policy, not semantics — nothing about signalling guarantees which waiter runs. - It frames the signal-placement question honestly as a measurable micro-optimization rather than a correctness rule people can argue about indefinitely.
- If Hoare semantics let waiters skip the re-test loop, why did mainstream systems adopt Mesa instead?Cost and practicality. Hoare requires an atomic lock-and-CPU handoff plus an urgent queue, which forces at least two extra context switches per signal and defeats fast uncontended lock paths. It also suspends the signaller mid-critical-section, so its own invariants must hold at the signal point. Mesa keeps signalling cheap and lets the signaller finish coherently, at the cost of one while loop in the waiter — a trade almost everyone considers worthwhile.
- What is barging, and how does it relate to these semantics?Barging is a thread that never waited acquiring the mutex ahead of a thread that was just woken. It is possible precisely because signal-and-continue does not hand the lock to the woken thread, so the woken thread must queue for it like anyone else. Implementations permit it deliberately, since letting an already-running thread take a briefly-free lock avoids a context switch — the price is that a specific waiter has no fairness guarantee and its predicate may be false when it finally runs.
- Is it wrong to signal after releasing the mutex?No, provided the state change that makes the predicate true happened while holding the mutex — that is what prevents lost wakeups. Signalling after release avoids waking a thread straight into a lock the signaller still holds, but widens the window for barging and wasted wake-ups. It is a measurable scheduling trade-off rather than a correctness rule, and some platforms additionally require the lock to be held when signalling, so portability matters.
Hoare is a relay race where the runner stops dead and physically presses the baton into the next runner's hand; Mesa is shouting "your turn" across the track and carrying on — by the time they get there, someone else may have picked the baton up.
saying these in an interview costs you the question
- Believing mainstream condition variables hand the lock directly to the woken thread.
- Concluding that an if is acceptable because 'the signaller just made the predicate true'.
- Claiming signalling outside the mutex causes lost wakeups by itself — the hazard is mutating the state outside the mutex.
- Assuming signalling picks the longest-waiting thread or provides any fairness guarantee.
- Presenting signal placement as a correctness rule rather than a scheduling trade-off to measure.