The idea: two ways to wait for the same thing
A bounded buffer has N slots. A producer must wait while it is full; a consumer must wait while it is empty. The page builds this buffer twice and runs the same threads through both, one line per tick in each lane. On the left the waiting is done with two semaphores; on the right with a mutex, a counter and two condition variables.
Semaphore: a counter that remembers
A semaphore is a number plus a queue of sleepers. sem_wait takes one unit, or sleeps if the number is 0. sem_post wakes one sleeper, or, if nobody sleeps, adds 1. So a post that nobody waits for is kept, and a later wait returns at once. For the buffer, empty counts free slots and full counts items: the counters are the condition. A mutex still protects the slots themselves.
Condition variable: wait for your own predicate
A condition variable has no number at all. It is only a queue of sleepers tied to a mutex. cond_wait(&cv, &m) releases m and sleeps in one atomic step, and takes m back before it returns. cond_signal wakes one sleeper, cond_broadcast wakes all of them, and with nobody asleep the signal is simply lost. The condition — here count == 0 — lives in your own shared variable, and you check it while holding m.
Why the wait sits in a while loop
Because a condition variable forgets, a thread must look at the predicate before it sleeps: if the item is already there, it must not wait for a signal that has already gone by. Demo 2 writes the consumer like the semaphore one, waiting without checking count. The producer's signal comes first and is lost, and the consumer sleeps forever next to a full slot. The semaphore lane, same schedule, finishes: the early post was kept in full.
A thread must also look at the predicate after it wakes. POSIX allows a spurious wake-up: cond_wait may return although nobody signalled. Another thread may also have taken the item between the signal and the moment the woken thread gets m back. In Demo 3 the cond var wakes the consumer for no reason; the while loop sees count still 0 and puts it back to sleep. That is correct, but it is a wasted wake-up the scoreboard counts. A semaphore wait only returns with a unit in hand.
What a counter can't say
A semaphore can only count one kind of thing, and one sem_post wakes one sleeper. Two conditions break it:
- "Wake everybody." In Demo 4 the producer closes the buffer and every waiting consumer must notice and leave. With semaphores the producer would need one post per sleeper and has no way to know there are three; its single post lets one consumer out, and two sleep forever. The condition-variable version sets
closed = 1and callscond_broadcast: all three wake, check, and leave. - "Two slots at once." In Demo 5 each producer puts a 2-part message. With semaphores it calls
sem_wait(&empty)twice; P1 and P2 each get one of the two free slots and wait forever for a second one: a deadlock, with the buffer empty.while (N - count < 2)checks both slots in one step underm, so no producer ever holds half of what it needs.
For the plain buffer (Demo 1) both work and finish at the same tick. The condition-variable code makes fewer calls (19 vs 24), because a thread that does not have to wait only locks, checks, signals and unlocks; the semaphore code always pays for its sem_wait and sem_post. Its signals nobody hears are harmless here, because every wait checks count first.
When to pick which
| Semaphore | Mutex + condition variable | |
|---|---|---|
| What it holds | a count and sleepers | sleepers only; the state is yours |
| Post / signal with nobody waiting | kept (value + 1) | lost |
| Spurious wake-ups | no (only EINTR from a signal handler) | yes: always wait in a while loop |
| Wake everyone | no: one post per sleeper | cond_broadcast |
| Condition | "a unit is free", nothing else | any predicate on shared state |
| Owner | none: any thread may post | the mutex has one owner |
| Good for | pools of N resources, counting free slots, a one-shot signal, posting from a signal handler (sem_post is async-signal-safe) | queues and states with more than one condition, shutdown, waiting for "k units at once" |
Either can build the other: a semaphore is a counter guarded by a mutex and a condition variable, and the bounded buffer page runs both solutions with their classic bugs.
Common mistakes
- Treating a condition variable like a semaphore. Replacing
sem_wait(&full)withcond_wait(¬Empty, &m)drops the memory: an early signal is lost (Demo 2). ifinstead ofwhile. After a spurious or stolen wake-up the thread goes on although the predicate is false and takes from an empty buffer.- Several
sem_waitcalls for one need. Taking units one at a time from a semaphore lets two threads each hold part of what they need (Demo 5).
What the page leaves out
Each lane runs one line per tick, round-robin, as if every line were preempted; real threads run in parallel and the kernel decides who goes next. Calls are counted, not their cost: an uncontended sem_wait, lock or cond_signal is a user-space atomic, and only sleeping and waking enter the kernel (futex). The page leaves out sem_trywait and sem_timedwait, pthread_cond_timedwait, wait morphing, priority inversion, named and System V semaphores, Java's Semaphore and Object.wait, and C++'s std::counting_semaphore and std::condition_variable, which follow the same rules.