Why must a threading.Condition wait() be wrapped in a loop that rechecks the predicate?
answer
- Waking is not the same as being ready
- Who holds the lock after notify?
- State can change before you reacquire
- wait_for writes the loop for you
basics
~20 sReturning from threading.Condition.wait() only means the waiter reacquired the condition's lock — not that the state it wanted still holds. A notified thread can lose the race to another, and a timeout also returns, so the predicate must be rechecked in a while loop.
solid answer
~50 s`threading.Condition.wait()` atomically releases the condition's lock and parks the thread. When `notify()` runs, the waiter is only moved to the queue of threads *competing* for that lock — the lock is not handed to it. Between the notifier releasing the lock and the waiter reacquiring it, any other thread can run and consume the very state the waiter was signalled about. `notify_all()` makes this routine, since it wakes waiters that may be waiting on entirely different predicates. A `wait(timeout=...)` also returns normally when nothing was signalled. So the only correct shape is `while not predicate(): cond.wait()` inside `with cond:` — an `if` is a latent bug that appears under load. `cond.wait_for(predicate, timeout)` writes that loop for you and returns the predicate's last value, which is `False` if it timed out. You must hold the condition's lock to call `wait()`, `notify()` or `notify_all()`; otherwise they raise `RuntimeError`.
code
python · 18 linesimport threading
cond = threading.Condition()
items = []
def consumer():
with cond:
cond.wait_for(lambda: items)
print("consumer got", items.pop())
t = threading.Thread(target=consumer)
t.start()
with cond:
items.append("scored-request")
cond.notify()
t.join()go deeper
Recall the shape rather than the theory: acquire the condition with a with block, loop while not <predicate> around wait(), and mutate state before notifying. Knowing that if is wrong there already puts you ahead.
Explain the mechanics an interviewer is really testing: wait releases the lock atomically and must reacquire it before returning, so another thread can consume the state in between. Show wait_for, and say what it returns on timeout.
Demonstrate production judgement — one condition per predicate versus notify_all, how a lost wakeup presents as a hang rather than an error, and when a ready-made blocking channel is the better answer than hand-rolled coordination.
Own the policy: where in the system bespoke condition variables are allowed at all, versus standardising on higher-level channels that cannot be misused. Be ready to argue about the reviewability of coordination code as a team-scale risk.
A `threading.Condition` is a lock plus a wait set. It exists to solve one problem: a thread needs to block until some shared state becomes true, without burning CPU polling and without missing the moment the state changes. ### What wait() actually does You must hold the condition's lock to touch it — `with cond:` — and every method raises `RuntimeError` if you do not. Inside that block: 1. `wait()` **atomically** releases the lock and adds the calling thread to the condition's wait set. The atomicity matters: if the release and the park were separate steps, a notifier could squeeze in between and signal a thread that has not yet parked, and the wakeup would be lost forever. 2. Another thread takes the lock, mutates the shared state, and calls `notify()` (one waiter) or `notify_all()` (every waiter). 3. The woken thread does **not** get the lock from the notifier. It is moved from the wait set to the ordinary contention for the lock, and `wait()` returns only once it has reacquired it — which happens after the notifier's `with` block exits, and after any other thread that was already queued for that lock has had its turn. Step 3 is the whole answer. The notification tells you "the state changed at some point"; it cannot tell you "the state is still what you want". Three concrete ways the predicate is false again by the time you look: * **Barging.** A third thread was already blocked in `acquire()` on the condition's lock. It gets in before the woken waiter, takes the one available item, and leaves. The waiter wakes to an empty buffer. * **notify_all with mixed predicates.** A bounded buffer has producers waiting for "not full" and consumers waiting for "not empty" on the same condition. `notify_all()` wakes both groups; most of them find their own predicate still false and must go back to waiting. * **Timeouts.** `wait(timeout=0.5)` returns after the timeout with nothing signalled at all. It returns `False` in that case and `True` otherwise, but code that ignores the return value cannot tell the difference — unless it rechecks the predicate. ### The correct shape ```python with cond: while not items: # never `if` cond.wait() item = items.pop() ``` and the producer side: ```python with cond: items.append(job) cond.notify() ``` Note the producer mutates the state *while holding the lock*, then notifies. Notifying without holding the lock raises `RuntimeError` in Python; notifying before the state is visible is the classic lost-wakeup bug in languages that permit it. `cond.wait_for(predicate, timeout=None)` is the same loop, written once and correctly: it evaluates the predicate, waits, re-evaluates, and handles the shrinking remaining timeout across multiple wakeups. It returns the predicate's last value — `False` means it gave up. Prefer it; hand-rolled loops with timeouts almost always forget to subtract the elapsed time. ### notify versus notify_all `notify(n=1)` wakes at most `n` waiters and is cheaper, but it is only safe when every waiter is waiting for the *same* condition and any one of them can make progress. The moment two different predicates share one condition object, `notify()` can wake a thread that cannot proceed while the thread that could stays asleep — a lost wakeup that looks like a hang. The robust defaults are: one condition per predicate with `notify()`, or one condition with `notify_all()`. Reach for the fine-grained version only when profiling shows the thundering herd costs you. ### When not to use a Condition at all Most producer/consumer code should not hand-roll this. A ready-made thread-safe blocking channel already encapsulates the lock, the condition and the predicate loop, and it is far harder to get wrong. Use `Condition` when you need to wait on a predicate that no off-the-shelf container expresses — "at least three workers have registered", "the cache generation advanced past N" — or when several distinct predicates guard one piece of state. And never replace it with a `time.sleep()` polling loop: that trades correctness problems for latency and wasted CPU, and it still needs the recheck.
- What must a thread hold before calling wait(), notify() or notify_all() on a threading.Condition?The condition's own lock — normally by being inside `with cond:`. All three methods raise `RuntimeError` otherwise. `wait()` releases that lock while parked and reacquires it before returning, which is why the surrounding block still reads as a single critical section even though it was interrupted.
- When should you prefer notify_all() over notify() on a threading.Condition?Whenever more than one distinct predicate waits on the same condition object — for example producers waiting for space and consumers waiting for items. `notify()` might wake a thread that cannot proceed and leave the one that could asleep. Use `notify()` only when all waiters are interchangeable, and pay the thundering-herd cost of `notify_all()` otherwise.
- What does threading.Condition.wait_for return when its timeout expires?The predicate's last evaluated value, which is falsy in the timeout case — not an exception. So `if not cond.wait_for(ready, timeout=5): ...` is how you branch on giving up. It also manages the remaining timeout across repeated wakeups, which hand-rolled loops usually get wrong by restarting the full timeout each pass.
Being called from a waiting room does not mean the seat is still free — you have to walk to the door, and somebody nearer may have taken it. You check when you arrive, and sit back down if it is gone.
saying these in an interview costs you the question
- Uses if instead of while around Condition.wait
- Calls notify without holding the condition's lock
- Thinks notify hands the lock straight to the waiter
- Replaces wait/notify with a time.sleep polling loop
- Assumes wait returns only when the predicate is true
- Notifies before mutating the shared state