4.2.17Operating Systems

Monitors — condition variables, wait, signal, broadcast

1,940 words9 min readdifficulty · medium1 backlinks

WHAT is a Monitor?

WHY invent monitors? With raw semaphores, the lock acquire/release is scattered across your code by hand — forget one signal() and you deadlock forever. A monitor makes mutual exclusion structural: the compiler/runtime puts the lock around every procedure, so you can't forget it. You only reason about the conditions, not the locking.


WHAT is a Condition Variable?


The golden rule: WHY wait must be inside a while

Let's derive why while (not if) is mandatory. Suppose a consumer waits for count > 0:

  1. Consumer sleeps in wait because count == 0. It is off the CPU and outside the lock.
  2. Producer enters, sets count = 1, calls signal. Consumer is moved to "ready."
  3. But "ready" ≠ "running." Between the signal and the consumer actually re-acquiring the lock, a third thread can sneak in and consume that item, setting count = 0 again.
  4. The consumer finally runs — if it used if, it would proceed as if count > 0, but it's now 00! Bug.

With while, on wake-up the consumer re-tests the predicate, sees it false, and loops back to wait. Safe. This is also the only correct way to survive spurious wake-ups (some OSes wake threads for no reason) and broadcast (many wake, only one wins).


HOW wait achieves atomicity (the crux)


Mesa vs. Hoare semantics — WHO holds the lock after signal?

Almost all real systems (Java, pthreads, Python threading.Condition, Go) use Mesa. So in practice: always while.

Figure — Monitors — condition variables, wait, signal, broadcast

WORKED EXAMPLE 1 — Bounded Buffer (Producer/Consumer)

monitor BoundedBuffer:
    buf[N], count = 0
    condition notFull, notEmpty
 
    procedure produce(item):
        while (count == N)        # Why? buffer full → can't add. while, not if (Mesa).
            wait(notFull)         # Why? drop lock + sleep until space appears.
        buf.add(item); count++    # Why? safe: we hold the lock, count < N proven.
        signal(notEmpty)          # Why? a consumer may be sleeping on empty buffer.
 
    procedure consume():
        while (count == 0)        # Why? nothing to take → wait.
            wait(notEmpty)
        item = buf.remove(); count--
        signal(notFull)           # Why? we just freed a slot; a producer may wait.

Why two CVs? notFull and notEmpty represent different conditions. If you used one CV and signal, you might wake a producer when only a consumer can proceed → it re-sleeps, wasting a wakeup, possibly deadlocking. Separate CVs = wake exactly the right group.


WORKED EXAMPLE 2 — When you MUST use broadcast

Suppose a single CV ready guards a resource pool, and waiting threads need different amounts of memory. A thread frees 10 MB.

  • signal(ready) wakes one thread. What if it needs 100 MB and can't proceed, but another waiter needs only 5 MB and could? The 5 MB thread stays asleep → wasted wakeup / starvation.
  • broadcast(ready) wakes all; each re-tests its own predicate in its while; the one(s) that can proceed do, the rest go back to sleep. Correct.

WORKED EXAMPLE 3 — The lost-signal bug (and fix)

# BROKEN consumer
if (count == 0)        # ← if, not while
    wait(notEmpty);
take();

Why it breaks: Under Mesa, a third thread can empty the buffer between signal and re-acquire (the derivation above). Then take() runs on empty buffer → crash. Fix: change ifwhile. One character. Re-tests after wake.


Recall Feynman: explain to a 12-year-old

Imagine a kitchen where only one cook fits at a time (that's the monitor lock). A cook wants to bake but there are no eggs. Instead of standing there blocking the door, the cook steps out and takes a nap on a special bench (wait), so other cooks can use the kitchen. When someone brings eggs, they ring a bell (signal) to wake one napping cook (or broadcast to ring for everyone). The woken cook walks back in but must re-check the fridge — because someone else might have grabbed the eggs first. That re-check is the while loop!


Active Recall

What two things does a monitor automatically bundle together?
Shared data + procedures protected by one automatic mutual-exclusion lock (only one thread inside at a time).
Why must wait be wrapped in a while, not an if?
Under Mesa semantics the predicate can become false again between signal and re-acquiring the lock; while re-tests it (also handles spurious wakeups & broadcast).
What three sub-actions does wait(cv) perform, and why must they be atomic?
Release lock, enqueue on CV, sleep — atomic to avoid the lost-wakeup race where a signal fires before the thread is asleep.
Difference between Hoare and Mesa signalling?
Hoare: signaller immediately hands lock to woken thread (predicate guaranteed true → if ok). Mesa: signaller keeps running/lock, woken thread just becomes ready (predicate stale → while).
Why is a condition variable NOT like a semaphore counter?
A CV is memoryless: a signal with no waiters is lost forever; a semaphore's value remembers the V().
When should you use broadcast instead of signal?
When multiple waiters wait on different predicates sharing one CV, or when one state change can satisfy several waiters; broadcast lets each re-test and proceed.
Why use two condition variables (notFull, notEmpty) in a bounded buffer?
They represent distinct conditions; signalling the correct one avoids waking a thread that can't proceed, preventing wasted wakeups/deadlock.
What real systems use Mesa semantics?
pthreads, Java, Python threading.Condition, Go — essentially all of them.

Connections

  • Semaphores — lower-level primitive; monitors are often built on top of them.
  • Mutual Exclusion — the automatic guarantee a monitor provides.
  • Producer-Consumer Problem — canonical monitor use case.
  • Deadlock — wrong CV choice / missed signal can cause it.
  • Race Conditions — lost-wakeup is the race wait atomicity prevents.
  • Spurious Wakeups — second reason while is mandatory.

Concept Map

provides

contains

only one thread inside

is

atomically drops lock

blocks caller on

wakes one from

wakes all from

is

requires

requires

re-tests before

Monitor safe room

Automatic mutex lock

Condition variable

Queue of sleeping threads

wait

signal

broadcast

while predicate loop

Memoryless - lost signal

Spurious wake-ups

Hinglish (regional understanding)

Intuition Hinglish mein samjho

Socho ek monitor ek "safe kamra" hai jisme ek hi chaabi hai — matlab ek time pe sirf ek thread andar ja sakta hai. Mutual exclusion automatic mil jaata hai, tumhe lock manually lagana-hatana nahi padta (semaphore mein bhool gaye toh deadlock). Jab andar wala thread dekhta hai ki abhi kaam ho hi nahi sakta (jaise buffer khaali hai), toh wo wait(cv) call karta hai — yani chaabi chhod ke so jaata hai condition variable ki queue mein. Ab koi aur thread andar aa ke duniya badal sakta hai, aur phir signal se sone wale ko jaga deta hai.

Sabse important baat: wait hamesha while loop ke andar hona chahiye, kabhi if mein nahi. Kyun? Kyunki real systems Mesa semantics use karte hain — signal milne ke baad bhi tum turant nahi chalte, sirf "ready" hote ho. Is beech koi teesra thread aa ke predicate dobara false bana sakta hai. Isliye uthne ke baad tumhe khud dobara check karna padta hai. Yahi while ka kaam hai. Ek aur reason: spurious wakeup (OS bina wajah jaga deta hai).

signal ek thread ko jagaata hai, broadcast sabko. broadcast tab use karo jab alag-alag waiters alag-alag conditions pe wait kar rahe ho ek hi CV pe — sabko jagaao, har koi apna predicate re-check karega, jo proceed kar sakta hai wahi karega. Aur yaad rakho: CV ki memory nahi hoti — agar koi soya hi nahi tha aur tumne signal kiya, toh wo signal hamesha ke liye gaya. Isliye predicate hamesha khud check karna, signal pe bharosa mat karna.

Bounded buffer mein do CV use karte hain — notFull aur notEmpty — kyunki producer aur consumer alag conditions ka intezaar karte hain. Sahi CV ko signal karne se galat thread nahi jagta, aur deadlock se bach jaate ho. Yeh pattern interviews aur OS exams mein bahut poocha jaata hai.

Test yourself — Operating Systems

Connections