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?
answer
- signal = hint, not a guarantee
- barging: a non-waiting thread grabs the lock first
- spurious wakeups are allowed by spec
- broadcast wakes many, most predicates false
- while, never if; absolute deadline for timed waits
basics
~20 sWaking 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.
solid answer
~60 sA wake-up carries no promise. Three independent reasons force the loop: 1. **Stolen wakeup (the barging window).** Signalling and re-acquiring the mutex are not one step. Between the producer's signal and the woken thread actually getting the lock, another thread can enter the critical section and consume exactly the state that made the predicate true. The woken thread then finds an empty queue. 2. **Spurious wakeups.** Most condition-variable specifications explicitly allow `wait` to return without any signal — a concession to efficient implementations over OS primitives and signal handling. Correct code must tolerate it. 3. **Multiple predicates on one condition.** If producers and consumers share a condition variable, or a broadcast wakes everyone, most woken threads' predicates are false. They must re-check and go back to sleep. Hence the invariant shape: `while (!predicate) cond.wait(mutex);` — the predicate is re-tested while holding the lock, so the check and the subsequent action are atomic with respect to other threads. Using `if` yields a thread proceeding on a false predicate: popping from an empty queue, exceeding a bound, or dereferencing a null.
code
text · 7 linesC1: lock(m); while empty: wait(m) // C1 sleeps, m released
P : lock(m); push(x); signal(); unlock(m) // C1 moved to the mutex queue
C2: lock(m) // C2 barges: never waited
C2: pop(x); unlock(m) // takes the item C1 was signalled for
C1: acquires m at last -> queue EMPTY again
with WHILE -> loops and waits again (correct)
with IF -> pops from an empty queue (corruption)go deeper
Recall the rule and one concrete failure: with if, a woken consumer can pop from a queue another consumer already emptied. Say the predicate must be re-tested while holding the lock.
Give all three causes — stolen wakeup from barging, spurious wakeups, and one condition serving several predicates — and show the canonical while-loop shape.
Explain the Mesa signal-and-continue model that makes barging normal, why unfair mutexes are chosen deliberately, and add the symmetric obligation that every state change must signal, plus the absolute-deadline pattern for timed waits.
Treat it as an invariant-based design rule — the predicate over guarded state is the contract; signals are only hints — and argue for encapsulating the pattern in reviewed building blocks rather than hand-rolling monitors at each call site.
## The rule ``` lock(m) while not predicate(): // WHILE, never IF cond.wait(m) act_on(state) // predicate verified under this same lock hold unlock(m) ``` This shape is not defensive style; it is a correctness requirement. Every one of the three reasons below is sufficient on its own. ## Reason 1: the wake-up is not atomic with the state change (Mesa semantics) Under the *signal-and-continue* (Mesa) semantics used by essentially every modern platform, signalling does not transfer the lock or the CPU to the woken thread. Sequence: ``` P: lock(m); push(x); signal(); unlock(m) C1: (was waiting) -> moved to the mutex queue, now competing for m C2: (never waited, just arrived) lock(m); sees non-empty; pop(x); unlock(m) C1: finally acquires m ... queue is empty again ``` C2 *barged*: it never waited, so it was never in the wait set, but it beat C1 to the lock. This is not a bug in C2 — it is the normal outcome of an unfair mutex, and unfair mutexes exist because they are dramatically faster (they avoid a context switch when the lock is briefly free). The consequence is precise: **a signal tells you the predicate was true at some moment in the past.** Only re-testing under the lock tells you it is true *now*, and holding the lock from the test through the action is what makes the answer still valid when you act. ## Reason 2: spurious wakeups are legal Condition-variable specifications generally permit `wait` to return with no corresponding signal. Reasons include implementations layered on OS futex-style primitives where a wait can be interrupted (by a process signal, or during a fork), and optimizations where waking an extra thread is cheaper than proving exactly one should wake. Rather than pay for the guarantee, the specifications push the cost to callers — who already need the loop for reason 1 anyway, so it is free. A candidate who cites *only* spurious wakeups has learned the rule but not the mechanism. Stolen wakeups are more common in practice and would force the loop even on a platform that promised no spurious ones. ## Reason 3: one condition, several predicates If a bounded buffer uses one condition variable for both "not empty" and "not full", or if the code broadcasts, then most woken threads are the wrong ones. Their predicates are false; they must loop and sleep again. Even with one predicate, a broadcast that wakes ten consumers for one produced item means nine must re-check and return to waiting — a *thundering herd* that is correct precisely because of the loop. ## What breaks if you use `if` The failure is not a hang — it is worse. The thread proceeds with a false predicate: - `pop()` on an empty queue → an exception, a null, or a corrupted index. - A bounded buffer overrun past its capacity. - Work dispatched against a resource another thread already claimed, producing duplicate processing or double-free-style bugs. And it is timing-dependent: with one producer, one consumer and no contention it may pass every test, then fail under load in production. That is why the loop is treated as non-negotiable in review, independent of any argument that "in this case only one thread can wake". ## The predicate must be a function of guarded state Two related requirements often missed: 1. The predicate must read only state protected by the *same* mutex. Testing a variable that other threads mutate without that lock re-introduces a race between the test and the action. 2. Every code path that can make the predicate true must signal (or broadcast) that condition. The loop protects you from *spurious and stolen* wake-ups; it cannot rescue you from a state change that was never announced. Missing signals are the other half of the discipline. ## Timeouts fold into the same loop A bounded wait does not change the shape, it only adds a deadline test: ``` deadline = now() + timeout lock(m) while not predicate(): remaining = deadline - now() if remaining <= 0: unlock(m); return TIMED_OUT cond.wait(m, remaining) // may return early; loop re-checks anyway act(); unlock(m) ``` Note the absolute deadline. Re-passing the *original* relative timeout on each iteration is a classic bug: with repeated spurious or stolen wakeups the total wait can extend without bound. ## Summary in one line A signal is a **hint that the state may have changed**, not a **transfer of a satisfied condition**. The loop converts the hint into a verified fact, under the lock, at the moment you act on it.
- On a platform that guaranteed no spurious wakeups, could you use if instead of while?No. The stolen-wakeup case remains: signalling does not hand the mutex to the woken thread, so any thread that arrives and acquires the lock first can consume the state that made the predicate true. The woken thread would then act on a false predicate. Only a strict signal-and-hand-off (Hoare) implementation with no barging would make an if safe, and that is not how mainstream platforms behave.
- Does waiting in a loop protect you from a producer that changes state but forgets to signal?No, and this is the complementary bug. The loop makes wake-ups safe; it does nothing about wake-ups that never happen. A thread whose predicate became true without a signal simply sleeps forever. The discipline is symmetric: every code path that can make a waiter's predicate true owes a signal or broadcast on the matching condition, under the same mutex.
- Why is re-passing the original relative timeout on each loop iteration a bug?Because each spurious or stolen wakeup restarts the full timeout, so the total time waited can grow far beyond what the caller asked for — in a busy system, effectively without bound. Compute an absolute deadline once before the loop and wait for the remaining interval each iteration, returning a timeout result when nothing remains.
Being called from a waiting room does not mean a seat is still free — someone standing by the door may have taken it while you were walking over. You check the board yourself when you arrive, and if it is gone you sit back down.
saying these in an interview costs you the question
- Citing spurious wakeups as the only reason for the loop, missing the more common stolen/barging case.
- Claiming if is safe when there is exactly one waiter and one signaller.
- Testing a predicate that reads state not protected by the same mutex.
- Assuming the signaller hands the lock directly to the woken thread.
- Re-passing the original relative timeout inside the loop instead of tracking an absolute deadline.