How should a threading.Condition wait recompute its timeout after an early wake-up?
answer
- A wait can return before it should
- The loop passes the timeout again
- Convert a budget into a fixed point
- Subtract now from that point each pass
- Capture the deadline from time.monotonic()
basics
~20 sCompute an absolute deadline once as time.monotonic() plus the timeout, then on each pass pass remaining = deadline - time.monotonic() to the wait and give up when it drops to zero or below. Re-passing the original timeout makes the total wait unbounded.
solid answer
~50 s`threading.Condition.wait(timeout)` can return before the timeout elapses — because it was notified while the predicate is still false, or because of a spurious wake-up — so the wait always lives inside a `while not predicate:` loop. The trap is what you pass on the second iteration. Handing it the original timeout again restarts the budget every time, so a nominal 50 ms wait can run indefinitely under a stream of notifications. The fix is an absolute deadline captured once: `deadline = time.monotonic() + timeout`, then each pass computes `remaining = deadline - time.monotonic()`, bails out if that is not positive, and waits on the remainder. Use `time.monotonic()`, never `time.time()`: a clock step mid-wait would otherwise expire the deadline instantly or push it an hour out. `Condition.wait_for(predicate, timeout)` implements exactly this loop and returns the predicate's final value, so prefer it unless you need custom behaviour.
code
python · 19 linesimport threading
import time
ready = threading.Condition()
pick_lines: list[str] = []
def wait_for_batch(minimum: int, timeout: float) -> bool:
deadline = time.monotonic() + timeout
with ready:
while len(pick_lines) < minimum:
remaining = deadline - time.monotonic()
if remaining <= 0:
return False
ready.wait(remaining)
return True
print(wait_for_batch(340, 0.05)) # False, after about 50 ms in totalgo deeper
Recall that a wait with a timeout can return early and must sit inside a loop that rechecks the condition. Know that a timeout is a per-call limit, not automatically a total budget.
Explain the mechanics of the fix: capture deadline = time.monotonic() + timeout once, compute the remaining time each pass, exit when it is not positive, and know that Condition.wait_for() already does this.
Demonstrate having debugged it. Describe how frequent notifications turn a re-passed timeout into an unbounded wait, why the deadline must come from a monotonic clock, and how the same shape covers retries and chained blocking calls.
Own the convention. Decide that timeouts propagate as deadlines across layers of a system, that every blocking boundary derives its remaining time from one origin, and how those deadlines are surfaced, observed and tested.
## Why the loop exists at all `threading.Condition.wait()` releases the condition's lock, blocks, and reacquires the lock before returning. It can return for reasons other than "the thing you were waiting for happened": another thread called `notify()` when your particular predicate was still false, several waiters were woken and another one consumed the work, or the platform delivered a spurious wake-up. Nothing about a return from `wait()` proves the predicate is now true. That is why the correct shape is always: ```python with cond: while not predicate(): cond.wait(timeout) ``` Given a timeout, `wait()` returns `False` if the timeout elapsed and `True` otherwise — but that return value tells you about the wait, not about your predicate, so the loop still has to check. ## The bug: re-passing the original timeout Look at what the loop above does when the predicate stays false. Each iteration passes the *full* `timeout` again. If notifications arrive every 10 ms and the predicate keeps evaluating false, a wait the caller believes is capped at 50 ms never terminates. The timeout has become a per-iteration limit instead of a total budget, and the caller's contract is silently broken. Under load — exactly when notifications are frequent — the wait gets longer, which is the worst possible failure direction. A second version of the same bug is subtler: keeping a running float budget and subtracting each measured slice from it. Take a warehouse pick-list builder whose collector waits for a batch of 340 pick lines before flushing. If each loop measures how long it slept and does `budget -= elapsed`, every iteration contributes its own rounding, and across many short wake-ups that floating-point drift accumulates in an unpredictable direction — the effective timeout is neither the requested one nor a stable one, and two runs of the same regression pack disagree about when the collector gave up. ## The fix: one absolute deadline, recomputed remainder Capture the deadline once, from a clock that cannot jump, and derive the remaining wait from it on every pass: ```python deadline = time.monotonic() + timeout with cond: while not predicate(): remaining = deadline - time.monotonic() if remaining <= 0: return False cond.wait(remaining) return True ``` Two properties fall out. First, the total wait is bounded by `timeout` no matter how many times the thread wakes, because every iteration measures against the same fixed origin — there is nothing to accumulate and therefore nothing to drift. Second, the check `remaining <= 0` before waiting handles the case where the deadline has already passed, which matters because passing a negative timeout is not a well-defined way to say "do not wait". ## Why the clock must be monotonic Computing `deadline = time.time() + timeout` reintroduces the wall clock's ability to jump into the middle of a blocking wait. An NTP step backwards while the thread is blocked leaves a deadline that is now further away than the caller asked for; a step forwards expires it immediately, and the waiter reports a timeout after a few milliseconds. `time.monotonic()` cannot move backwards and is not affected by the clock being set, which is exactly the guarantee a deadline needs. The one caveat worth naming is suspend: on some platforms the monotonic clock does not advance while the machine is asleep, so a laptop closed mid-wait may resume with the deadline barely moved. ## Do not hand-roll it if you do not have to `threading.Condition.wait_for(predicate, timeout)` does precisely this: it loops on the predicate, tracks an absolute deadline, waits on the remaining time, and returns the predicate's last value — `False` meaning it timed out. Most correct code should call that rather than re-deriving it. The hand-rolled form is worth knowing anyway, because the same shape appears everywhere a deadline crosses more than one blocking call: * a retry loop that must respect one overall budget across several attempts, rather than giving each attempt the full timeout * successive `queue.Queue.get(timeout=...)` calls that together must not exceed a caller's limit * a socket read loop where each read gets the remaining time, not the original * `asyncio.timeout()` (3.11+), which is the async-side expression of the same idea: it computes an absolute deadline against the event loop's own monotonic clock and applies it to everything inside the block, rather than to each await separately. ## What an interviewer is listening for The signal is not the code. It is whether you distinguish a *timeout* — a per-call parameter — from a *deadline*, an absolute point that survives across calls, and whether you know that any wait can return early so the predicate, not the return value, decides.
- What does threading.Condition.wait() returning True actually tell you?Only that the wait did not time out — it was woken. It says nothing about your predicate, which may still be false because another waiter consumed the work or because the wake-up was spurious. That is why the predicate check belongs in a `while` loop and the boolean is at best a hint for bookkeeping.
- Is there a standard library call that already implements this deadline loop?Yes: threading.Condition.wait_for(predicate, timeout) loops on the predicate, tracks an absolute deadline internally, waits on the remaining time each pass, and returns the predicate's final value — False meaning it timed out. Prefer it; hand-roll the loop only when you need behaviour it does not offer, such as logging each wake-up.
- How does the same deadline idea apply to a retry loop with several attempts?Identically. Give the whole operation one deadline from time.monotonic(), and give each attempt `min(per_attempt_timeout, deadline - time.monotonic())`, abandoning the loop when the remainder is not positive. Otherwise every attempt gets the full timeout and a three-attempt retry silently costs three times the budget the caller agreed to.
A timeout is a stopwatch you restart every time you look up; a deadline is a train departure time. Only one of them still gets you home.
saying these in an interview costs you the question
- Passes the original timeout on every loop iteration
- Assumes a wait only returns when the predicate is true
- Builds the deadline from time.time() instead of a monotonic clock
- Subtracts measured slices from a running float budget
- Treats wait() returning True as proof the work arrived
- Passes a negative remaining time to the next wait