skip to content

questions

5

A consumer thread must wait until a shared queue becomes non-empty before taking an item. Explain the monitor pattern — a mutex paired with a condition variable — and why it is preferred to a loop that sleeps briefly and re-checks the queue.

level: juniorimportance: must knowfreq 58%

answer

  1. monitor = mutex + condition variable(s)
  2. wait atomically releases lock and sleeps
  3. signal is not stored — no waiter, no effect
  4. always wait inside a while over the predicate
  5. poll = latency + CPU + contention on the producer's lock

basics

~20 s

A monitor is a lock protecting shared state plus a condition variable to wait on. The consumer locks, and while the queue is empty calls wait, which atomically releases the lock and sleeps. A producer adds an item under the lock and signals. Polling wastes CPU and adds latency; waiting costs nothing until woken.

solid answer

~50 s

A **monitor** bundles two things: a mutex that gives mutually exclusive access to some shared state, and one or more **condition variables** that let a thread block until that state satisfies a predicate. The consumer's shape is always the same: ``` lock(m) while queue.isEmpty(): // loop, not if notEmpty.wait(m) // atomically: release m, sleep; on wake, re-acquire m item = queue.pop() unlock(m) ``` The producer pushes under the same mutex and then signals `notEmpty`. The critical property is that `wait` **atomically** releases the mutex and blocks. If it did not, the consumer would either hold the lock while sleeping — so no producer could ever add an item, a guaranteed deadlock — or drop the lock first and risk missing a signal in the gap. Compared with sleep-and-poll, waiting burns no CPU while blocked, wakes with latency measured in microseconds rather than the poll interval, and scales: a hundred idle consumers cost nothing, whereas a hundred pollers keep a core busy doing nothing.

code

text · 15 lines
text
mutex m; cond notEmpty; queue q

consumer:
    lock(m)
    while q.isEmpty():
        notEmpty.wait(m)        // release m + sleep, atomically; re-acquire on wake
    item = q.pop()
    unlock(m)
    handle(item)                // do slow work OUTSIDE the lock

producer(item):
    lock(m)
    q.push(item)
    notEmpty.signal()
    unlock(m)

go deeper

for a junior

Give the shape — lock, while-not-predicate wait, act, unlock — and say wait releases the lock while sleeping. Name the two reasons polling is worse: wasted CPU and worse latency.

for a middle

Explain the atomic release-and-enqueue and the lost wakeup it prevents, why the loop is required, and why a bounded buffer needs two conditions on one mutex.

for a senior

Add the operational angle: polling contends with the very lock producers need, signalling is an obligation of every state-changing path, and slow work belongs outside the lock.

for a principal

Frame monitors as the base construction beneath most blocking data structures, and discuss when to stop hand-writing them in favour of a well-tested blocking queue or a message-passing design that removes shared mutable state entirely.

## The problem: waiting for a *state*, not for a lock A mutex answers "may I touch this data right now?" It does not answer "has this data become what I need?" A consumer holding the lock over an empty queue cannot make progress, and holding the lock is actively harmful — it prevents the very producer that would help it. We need a way for a thread to say: *give up the lock, put me to sleep, and wake me when something relevant changes*. That is a condition variable, and a mutex plus its condition variables is a **monitor**. ## The three operations - **wait(mutex)** — must be called while holding the mutex. Atomically: add the caller to the condition's wait set, release the mutex, and block. On waking, re-acquire the mutex before returning. The caller therefore always holds the lock both before and after `wait`, and never during. - **signal() / notify()** — wake at least one thread in the wait set, if any. If none is waiting, it does nothing at all — the signal is *not* remembered. - **broadcast() / notifyAll()** — wake every thread in the wait set. ## Why atomicity of release-and-sleep is the whole trick Imagine `wait` were two separate steps — unlock, then sleep: ``` consumer producer check empty -> true unlock(m) lock(m); push(item); signal(); unlock(m) sleep() <- signal already happened; nobody was waiting ... sleeps forever, with an item sitting in the queue ``` That is the **lost wakeup**. Because a signal targets only threads *currently* in the wait set and is not stored, any window between deciding to wait and actually being in the wait set is a hole a signal can fall through. Making the release and the enqueue-into-the-wait-set atomic with respect to the mutex closes it: a producer cannot signal without holding the mutex, and it cannot hold the mutex until the consumer is already registered as waiting. This is why a condition variable is inseparable from *the* mutex that guards the predicate. A condition variable alone is not a usable primitive; "which lock does it pair with?" always has exactly one answer per predicate. ## Why the wait is inside a while loop `while queue.isEmpty()` rather than `if`. Being woken means "the state you care about *may* have changed", never "your predicate now holds". Between the signal and the woken thread re-acquiring the mutex, another consumer may have taken the item; some platforms also allow *spurious* wakeups with no signal at all. Re-testing the predicate under the re-acquired lock is the only correct discipline, and it costs one comparison. ## Why not sleep-and-poll? A polling consumer looks superficially similar: ``` loop: lock(m); item = queue.pop_or_null(); unlock(m) if item != null: handle(item); else sleep(10ms) ``` It is worse on four axes: 1. **Latency.** Average wake-up delay is half the poll interval. Shrinking the interval to reduce latency directly increases waste. 2. **CPU.** Each poll acquires the lock, touches shared cache lines, and returns. With many consumers this is measurable burn on an *idle* system — the worst kind of cost, since it appears when there is nothing to do. 3. **Contention.** Pollers repeatedly take the same mutex the producers need, so polling actively slows down the work it is waiting for. This is a real self-inflicted throughput loss under load. 4. **Power and density.** Sleeping threads let cores idle. Polling threads keep cores awake, which matters on battery, on shared/virtualized hosts, and for anything billed by CPU. The monitor pattern instead costs nothing while waiting and wakes with latency set by the scheduler, typically microseconds. ## The producer side, and where to signal ``` lock(m) queue.push(item) notEmpty.signal() // predicate is already true when the waiter re-checks unlock(m) ``` Signal while holding the lock (simplest and always correct) or immediately after releasing (can avoid the woken thread waking straight into a locked mutex). What is *not* optional: the state change and the signal must be paired. Every place that can make a waiter's predicate true owes a signal; a code path that mutates state and forgets to signal is exactly how systems hang. ## Bounded buffer: two conditions, one mutex A fixed-capacity queue needs two predicates — "not empty" for consumers, "not full" for producers — and therefore two condition variables sharing one mutex. Using a single condition for both is a classic bug source: a signal intended for a consumer can wake a producer, which re-checks its own predicate, finds it false, and goes back to sleep — while the consumer that should have run stays asleep. That is why *one condition variable per predicate*, or an unconditional broadcast, is the safe rule. ## Terminology note "Monitor" originally described a language-level construct where a whole object's methods run under an implicit lock with attached condition variables. Modern practice usually assembles the same thing explicitly from a mutex and condition variables. The concept — mutual exclusion plus condition-based waiting, in one package — is identical.

  • What would go wrong if wait() did not release the mutex?
    The waiting thread would sleep while still holding the lock, so no producer could enter the critical section to change the state or send the signal. The system deadlocks immediately: the only thread that could wake the sleeper is blocked on the lock the sleeper holds. Releasing the mutex is what makes waiting for a condition different from simply blocking on a lock.
  • Why must the release of the mutex and the enqueue into the condition's wait set be atomic?
    Because a signal only reaches threads already in the wait set and is never stored. If the mutex were released before the caller registered as a waiter, a producer could acquire the mutex, change the state, and signal in that window, and the consumer would then sleep with its predicate already true — a lost wakeup, and an indefinite hang. Atomicity means a producer cannot hold the mutex until the waiter is safely registered.

A doctor's waiting room: the consulting room holds one person at a time (the mutex), and instead of knocking every ten seconds to ask if the doctor is free (polling), you sit down and the receptionist calls your name (the condition variable). You still check the board when called, because someone else may have taken the slot.

saying these in an interview costs you the question

  • Calling wait without holding the associated mutex, or pairing one condition variable with different mutexes.
  • Believing a signal is queued and delivered to a thread that waits later — it is dropped if no one is waiting.
  • Using if instead of while around the wait.
  • Claiming sleep-and-poll is equivalent as long as the interval is short, ignoring latency, CPU burn, and added contention on the producer's lock.
  • Using one condition variable for two different predicates, so the wrong thread is woken and the right one keeps sleeping.

context

open as a page

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?

level: middleimportance: must knowfreq 70%

basics

~20 s

Waking 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.

open as a page

When releasing threads blocked on a condition variable, when is waking a single waiter correct and when must you wake all of them? Describe what goes wrong in each direction.

level: middleimportance: should knowfreq 48%

basics

~20 s

Waking one is safe only when every waiter on that condition waits for the same predicate and one unit of state satisfies exactly one waiter. Otherwise wake all: the single wake-up can reach a thread whose predicate is false, which consumes it and leaves the right thread asleep. Waking all is always correct but costs a thundering herd.

open as a page

A worker occasionally blocks forever waiting for data that was, in fact, produced. Explain the lost-wakeup bug on a condition variable and the exact locking protocol between the producer and the waiter that prevents it.

level: seniorimportance: should knowfreq 45%

basics

~20 s

A signal is not stored: if it fires when nobody is in the wait set, it vanishes. If the producer changes state and signals in the window after the waiter tests its predicate but before it is registered as waiting, the waiter sleeps forever. Prevention: test the predicate and wait under the same mutex that the producer holds while changing state and signalling.

open as a page

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.

level: seniorimportance: nice to knowfreq 26%

basics

~30 s

Under 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.

open as a page