The question this answers
Why must the predicate be re-checked after wait returns, and which languages and APIs does that apply to?
Four consumer tasks waiting on one condition variable for a shared bounded queue, with a producer that adds a single item and calls notify_all.
The queue and its length under a mutex. The condition variable holds no state; the predicate queue.length > 0 is evaluated over the mutex-protected queue.
A task that returns from wait and proceeds does so only when the predicate it waited for is actually true at that instant, with the lock held.
Synchronization exists to preserve this sentence. If a schedule can make it false, the code is wrong no matter which primitive it uses.
One character, two entirely different programs
The difference between if and while here is not a style preference and not defensive programming. It is the difference between a program that is correct and one that is correct most of the time. wait may return for three distinct reasons, and only one of them means what the naive reading assumes.
First, a genuine notification aimed at you. Second, a *spurious* wakeup: the implementation returned with no notification at all. This is explicitly permitted — POSIX allows pthread_cond_wait to return spuriously, C++ says the same of std::condition_variable::wait, Java's Object.wait documents it, and Python's threading.Condition.wait does too. It happens for real implementation reasons, most commonly when a signal interrupts the underlying futex wait, and allowing it lets implementations be substantially faster on the common path. Third — and this one is not spurious at all, merely misunderstood — a genuine notification whose state was consumed by someone else before you re-acquired the mutex.
That third reason is the important one, because it exists in *every* implementation, including any that never wakes spuriously. notify_all with four waiters and one item means three of them will wake correctly, find nothing, and must go back to sleep. A barging task that never waited at all can take the item between the notify and your wake. So even if spurious wakeups were abolished tomorrow, while would still be mandatory — which is why the rule is better stated as "a wakeup is a hint to re-check" than as "guard against spurious wakeups".
1def get(self):2 with self._cond:3 if not self._items: # <-- checked exactly once4 self._cond.wait()5 return self._items.popleft() # <-- assumes the predicate is true now6 7# Three ways this line fails, all of them ordinary:8#9# 1. SPURIOUS WAKEUP. wait() returned with no notify. _items is still10# empty. popleft() raises IndexError from an empty deque.11#12# 2. notify_all() WITH ONE ITEM AND FOUR WAITERS. All four wake, all four13# reach popleft(). One succeeds; three raise IndexError - and they were14# all notified correctly, about an item that genuinely existed.15#16# 3. BARGING. A fresh consumer acquires the lock between the notify and17# this task re-acquiring it, and takes the item. Same IndexError.18#19# The stack trace points at popleft(). The cause is in another task, three20# steps earlier, and the queue was demonstrably non-empty at notify time.1def get(self):2 with self._cond:3 while not self._items and not self._closed:4 self._cond.wait() # may return for ANY reason; we do not care5 if not self._items:6 raise QueueClosed() # woken by close(), not by an item7 return self._items.popleft() # the predicate was true, under the lock8 9# The loop makes all three cases identical and harmless:10# spurious wake -> predicate false -> wait again11# lost the race -> predicate false -> wait again12# real item, won -> predicate true -> proceed13#14# Note the second clause. Once a second reason to wake exists (closing),15# the loop must distinguish them, and the code after the loop must handle16# "woken, predicate for items still false, but we should not wait again".17# This is why `while` scales to real systems and `if` does not: adding a18# second wake reason to an `if` version silently breaks it.19 20# C++ writes the same loop for you, and this form should be preferred:21# cv.wait(lk, [&]{ return !items.empty() || closed; });22# which is defined as: while (!pred()) cv.wait(lk);The while version treats a wakeup as what it is — a hint to look again — rather than as a promise about the state. That single reframing makes spurious wakeups, multi-waiter competition, barging and future additional wake reasons all collapse into one already-handled case. The if version is correct only when there is exactly one waiter, exactly one reason to wake, no barging and no spurious returns, which is a set of assumptions no codebase preserves for long.
Four waiters, one item
The schedule below is the everyday version of this, and it involves no spurious wakeup at all — every wake is a genuine notification. A producer adds one item and calls notify_all because it does not know how many consumers are parked. Four wake. One gets the item. Three find an empty queue.
With while, those three re-evaluate, find the predicate false, and park again. Total cost: three wasted wakeups and three lock acquisitions — the small thundering herd that is the price of notify_all and the reason to prefer notify when exactly one waiter can proceed per state change. With if, those three call popleft on an empty deque and raise.
The failure has a distinctive and misleading shape in an incident review: three exceptions fire simultaneously, all with identical stack traces pointing at the dequeue, at the exact moment work arrived. The natural reading is "the queue is corrupt". The actual cause is a one-character bug in the consumer, and the queue was in a perfectly valid state at every instant.
| # | Consumer 1 | Consumer 3 | Producer — one item | State |
|---|---|---|---|---|
| 1 | acquire; predicate false; wait() — parks | · | · | items=0 waiters=1 |
| 2 | · | acquire; predicate false; wait() — parks (4 waiters total) | · | items=0 waiters=4 |
| 3 | · | · | acquire lock; items.append(job-1) | items=1 waiters=4 |
| 4 | · | · | notify_all() — all four waiters become runnable | items=1 waiters=0 |
| 5 | · | · | release lock | items=1 lock=free |
| 6 | wait() returns; re-acquires lock; re-checks: items = 1 → proceed | · | · | items=1 lock=C1 |
| 7 | popleft() → job-1; release | · | · | items=0 lock=free |
| 8 | · | wait() returns; re-acquires lock; re-checks: items = 0 → WAIT AGAIN | · | items=0 waiters=1 |
| 9 | · | IF `if` WERE USED: popleft() on an empty deque | · | items=0 ✕ IndexError in three consumers at once, each correctly notified, at the moment work arrived. The trace blames the queue; the bug is one character in the consumer. |
| 10 | · | SPURIOUS CASE: wait() returns with no notify at all; re-checks: items = 0 → wait again | · | items=0 waiters=1 |
notify_all wakes everyone and there was one item. The loop turns all four scenarios — genuine win, genuine loss, barging loss and truly spurious return — into one code path. That collapse is the whole value: you stop reasoning about *why* you woke and reason only about whether you may proceed.Which languages and APIs this applies to
The permission to wake spuriously is explicit in the specifications, and the table below cites where. But the more useful framing for a reviewer is broader: any wait-until-a-condition API needs a loop, whether or not its documentation mentions spurious returns, because multi-waiter competition and barging produce the same requirement.
The exceptions are instructive. A future or promise resolves exactly once and remembers its value, so awaiting an already-resolved promise completes immediately and awaiting one twice gives the same answer — there is no predicate to re-check because the primitive carries the state. A latch, once released, stays released. A semaphore permit is stored, so an acquire that succeeds has genuinely taken something. These primitives do not need loops precisely because they hold state, which is the same property whose absence makes condition variables require one.
The practical rule for review: if the primitive is a *parking area* (condition variable, monitor, park/unpark), loop. If it is a *value* (future, latch, permit, resolved promise), do not — take the value.
1// [thread.condition.condvar]: wait "may block ... spuriously".2// The predicate overload exists precisely so you cannot get it wrong:3std::unique_lock lk(m);4cv.wait(lk, []{ return !q.empty() || done; }); // == while(!pred()) cv.wait(lk);5 6// The raw form, if you must write it yourself:7while (q.empty() && !done) cv.wait(lk);8 9// Timed variants return a status AND may still wake spuriously, so the10// predicate overload is doubly preferred:11cv.wait_for(lk, 100ms, []{ return !q.empty(); }); // returns pred() at the endThe standard explicitly permits spurious wakeup, and the predicate overload of wait is defined as the loop. Prefer it — there is no reason to hand-write the while in modern C++.
1# threading.Condition.wait: "may return ... spuriously" - and CPython2# provides wait_for(), which is the loop, written once and correctly:3with cond:4 cond.wait_for(lambda: items or closed) # == while not pred(): wait()5 ...6 7# Manual form:8with cond:9 while not items and not closed:10 cond.wait()11 12# asyncio.Condition has identical semantics and the same requirement:13async with cond:14 await cond.wait_for(lambda: bool(items))15 16# NOT needed for these - they carry state:17ev = threading.Event(); ev.wait() # stays set; no loop18sem.acquire() # a permit is taken; no loop19await some_future # resolves once, remembers; no loopwait_for(predicate) is the loop and should be the default. The distinction to hold onto: Event, Semaphore and futures carry state and need no loop; Condition parks and therefore does.
1// There is no condition variable in single-isolate JavaScript, because2// nothing preempts a synchronous block. Coordination is a promise, which3// carries its value - so no loop is needed:4const ready = loadIndex()5await ready // resolves once, remembered; awaiting again is instant6 7// Across workers with SharedArrayBuffer, Atomics.wait IS a parking8// primitive and DOES require the loop:9// returns 'ok' | 'not-equal' | 'timed-out' and may wake without a notify10while (Atomics.load(buf, 0) === 0) {11 Atomics.wait(buf, 0, 0) // must re-check; 'ok' is not a promise12}13// Atomics.wait is unavailable on the main browser thread (it would block14// the UI) - worker threads only.Two regimes again. Promises carry state, so no loop. Atomics.wait is a genuine parking primitive on shared memory and needs the same while as any condition variable.
1// Same runtime, but types can encode the rule so it cannot be skipped.2// Shape the API as "wait until predicate" rather than "wait":3 4async function waitUntil(5 cond: AsyncCondition,6 predicate: () => boolean,7): Promise<void> {8 while (!predicate()) await cond.wait() // the loop lives HERE, once9}10 11// Callers cannot write the `if` version, because there is no bare wait()12// exposed on the type they are given:13interface AsyncCondition { waitUntil(p: () => boolean): Promise<void> }14 15await queueCond.waitUntil(() => items.length > 0 || closed)16 17// Compare: Java's Object.wait() documents spurious wakeup and offers no18// predicate overload at all, which is why the `if` bug is most common there.The types cannot detect a spurious wakeup, but they can remove the API that lets you ignore one. Exposing only a predicate-taking waitUntil makes the if bug unrepresentable.
- POSIX, C++, Java and Python all explicitly permit spurious wakeup; C++ and Python ship predicate-taking overloads that write the loop for you, and Java does not — which is why the bug is most common in Java code.
- Even where spurious wakeup is impossible,
notify_allwith several waiters and barging arrivals produce the same requirement, so the loop is never optional. - Promises, futures, latches and semaphore permits carry state and are resolved or taken exactly once — these need no loop, and adding one is a sign the wrong primitive is in use.
Atomics.waitin JavaScript is a real parking primitive on shared memory and requires the loop, unlike every other JavaScript coordination construct.- The reviewable rule: parking primitives loop, value-carrying primitives do not.
Key points
waitmay return without any notification. POSIX, C++, Java and Python all explicitly permit it.- Even without spurious wakeups the loop would still be required, because
notify_allwakes several waiters for one item and a barging task can consume the state before you re-acquire the lock. - Reframe the wakeup: it is a hint to re-check, never a promise about the state. That framing makes all four wake reasons the same case.
- Prefer the predicate-taking overload —
cv.wait(lk, pred)in C++,cond.wait_for(pred)in Python — which is defined as the loop. - Java offers no predicate overload on
Object.wait, which is why theifbug is most common there. - The rule that generalises: parking primitives (condition variables, monitors,
Atomics.wait) need a loop; value-carrying primitives (futures, latches, semaphore permits) do not. - The
ifversion breaks the moment a second reason to wake is added, which is why it fails during maintenance even when it was correct when written.
The loop, answered
Every field is required, which is why no lesson here can recommend concurrency without naming the interleaving that breaks it, the complexity it adds, and the simpler thing to consider first.
- •
waitregisters the task on the condition variable's wait set, releases the mutex and blocks. - • The task may be made runnable by a
notify, by anotify_all, or by the implementation itself — for example when a signal interrupts the underlying futex wait and the implementation chooses to return rather than restart. - • Before
waitreturns, the mutex must be re-acquired, which can mean waiting behind the signaller and behind any task that was not waiting at all. - • During that re-acquisition window, any other task holding the lock may change the state, including consuming the very item the notification was about.
- • Therefore the state on return is unknown, and the only sound action is to re-evaluate the predicate with the lock held and either proceed or wait again.
- • Genuine notify, one waiter, nothing else runs: predicate true on return, proceed. The case the
ifversion handles. - •
notify_allwith four waiters and one item: one proceeds, three find the predicate false and wait again. Withif, three exceptions fire simultaneously. - • Barging: a fresh consumer takes the item between the notify and the woken task re-acquiring the lock. Predicate false on return, in an implementation that never wakes spuriously.
- • Truly spurious:
waitreturns with no notification sent at all. Predicate false, handled by the same loop with no special case. - • Two wake reasons: an item arrives and, separately, the queue closes. With
whileand a compound predicate, both are distinguished after the loop; withif, adding the second reason silently breaks the first. - • A timed wait expiring: returns with the predicate false and must be treated exactly like any other wake — re-check, then decide whether to retry or give up.
- •
waitguarantees the mutex is held when it returns. It guarantees nothing about the predicate. - • It does NOT guarantee that a notification was sent. Spurious return is permitted by every major specification.
- • It does NOT guarantee that a notification aimed at you was not consumed by someone else first.
- •
notify_allguarantees every current waiter becomes runnable. It does not guarantee any of them can proceed. - • A timed wait guarantees a bounded return; it does not guarantee the predicate is true on return, whether it timed out or not.
- • The predicate overload guarantees the loop is written correctly, which is the only guarantee in this lesson you actually get for free.
- •
notify_allwith N waiters produces N wakeups and N lock acquisitions, of which typically one succeeds. That is a small thundering herd, and on a hot queue it is measurable. See Thundering Herd. - • Prefer
notifywhen exactly one waiter can proceed per state change, and separate condition variables per predicate so a wake reaches tasks that can actually make progress. - • The re-check itself is cheap — a predicate evaluation under a lock already held — so the loop costs essentially nothing on the path where the predicate is true.
- • Spurious wakeups are rare enough in practice that their contention cost is irrelevant; the herd from
notify_allis the cost that actually shows up in a profile.
- • Proceeding on a false predicate — dequeue from an empty queue, read from an unready buffer, use a resource that has not been initialised.
- • Simultaneous identical exceptions across several waiters at the exact moment work arrives, which reads as data corruption and is not.
- • Maintenance breakage: an
ifversion that was correct with one wake reason is silently broken when a shutdown signal is added. - • Silent wrong results where the predicate guarded data validity rather than presence — the task proceeds with a partially initialised structure and no exception is raised at all.
- • Herd from
notify_allused wherenotifywas correct, wasting wakeups on every state change. - • A timed wait treated as "the predicate is now true", which is the same bug with an extra return value.
- • Always. The loop is the correct use of the primitive, costs one predicate evaluation, and makes every wake reason equivalent.
- • It future-proofs the code: adding a second reason to wake — shutdown, timeout, cancellation — requires no change to the waiting structure.
- • It removes an entire category of review discussion, because "is a spurious wakeup possible on this platform?" stops being a question anyone needs to answer.
- • It does not. The only cost is one predicate evaluation on a path that already holds the lock.
- • The related mistake worth avoiding is looping around a *value-carrying* primitive — awaiting a promise in a loop, or re-acquiring a semaphore permit you already hold — which signals that the wrong primitive is in use.
- • A loop with no timeout and no cancellation path can spin between spurious wakes indefinitely if the predicate never becomes true. That is not a spurious-wakeup problem; it is a missing deadline. See Timeouts.
- • Count wakeups against successful predicate evaluations. A large ratio means
notify_allis waking tasks that cannot proceed, and points at the primitive choice rather than at the loop. - • Grep for
wait()not immediately preceded by awhileon the same predicate — this is a mechanical, high-yield static check and several linters implement it. - • In Java specifically, review every
Object.wait()call: with no predicate overload available, each one is a hand-written loop that may not exist. - • Watch for exception clusters — several identical failures at the same instant, at the moment work arrived — which is the
if-bug signature rather than a data problem.
- • Essentially none. The loop is one keyword, and the predicate-taking overloads remove even that.
- • The real complexity is conceptual: understanding that a wakeup carries no information about the state, which is counter to how the API reads.
- • Compound predicates — items available *or* closed *or* cancelled — do add complexity after the loop, because each wake reason needs its own branch. That is inherent to having several reasons, not to the loop.
- • Use the predicate-taking overload rather than hand-writing the loop:
cv.wait(lk, pred)in C++,cond.wait_for(pred)in Python. Same semantics, no chance of writingif. - • Use a value-carrying primitive where the semantics allow: a future, a latch or a semaphore permit is taken once and needs no re-check. See Futures & Promises and Latches & Countdowns.
- • Use a standard blocking queue or channel, which encapsulates the loop entirely. See Concurrent Queues and Channels.
- • Wrap your own condition API so only a predicate-taking
waitUntilis exposed, making theifversion unrepresentable at every call site.
wait() in an if, or wait() in a while
lock()
if (items == 0): # checked once, before sleeping
cond.wait(lock) # releases the lock, sleeps, reacquires
take(item) # <- assumes the predicate is still true
unlock()| # | Producer | Consumer 1 | Consumer 2 | Runtime | State |
|---|---|---|---|---|---|
| 1 | · | lock(); if (items == 0) wait() | · | · | items=0 waiters=1 |
| 2 | · | · | lock(); if (items == 0) wait() | · | items=0 waiters=2 |
| 3 | lock(); items = 1; notifyAll(); unlock() | · | · | · | items=1 waiters=0 |
| 4 | · | proceed: take(item) | · | · | items=0 waiters=0 |
| 5 | · | · | proceed: take(item) | · | items=-1 waiters=0 ✕ two consumers took one item — items = -1 |
Producers, a bounded queue, consumers
The producer is faster than the consumer
What people believe, and what is true
Spurious wakeups are a theoretical concern that never happens in practice.
They are permitted and do occur, but that is beside the point: notify_all with several waiters and barging arrivals produce a false predicate on return in every implementation. The loop is required regardless.
If I use notify instead of notify_all, if is safe.
A barging task can still take the item between the notify and your re-acquisition of the lock, and a spurious return is still permitted. Neither depends on which notify you used.
The loop is defensive programming.
It is the specified way to use the primitive. wait is defined to return when it *may* be worth re-checking, not when the condition holds.
My if version has run in production for two years without a problem.
It has one waiter, one wake reason and light load. The first of those to change breaks it, and the failure will look like data corruption rather than a concurrency bug.
Go deeper
Overview
Waking up does not mean the thing you waited for is true. Check again, in a loop.
Practical
Write while (!predicate) wait(), or better, use the predicate-taking overload your language provides. Never if. Never assume a return value means the predicate holds.
Advanced
The rule generalises past spurious wakeups: parking primitives require a loop because the state can change between the wake and the re-acquisition; value-carrying primitives (futures, latches, permits) do not, because they hold what you waited for. If you find yourself looping around a future, you have the wrong primitive.
Internals
Spurious returns arise from the implementation: a signal interrupting the underlying futex wait, or a wake-all used internally to avoid tracking exactly which waiter to target. Permitting them lets the fast path avoid bookkeeping, and since a correct waiter must re-check anyway — because of barging — the specification gives up nothing by allowing it.