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.

The consumer of a bounded buffer written two ways. Semaphores: sem_wait(&full), lock, take, unlock, sem_post(&empty); the condition lives in the counters empty and full. Condition variable: lock, while (count == 0) cond_wait(&notEmpty, &m), take, cond_signal(&notFull), unlock; the condition lives in your own variable count.
Same consumer, two codes: a semaphore holds the condition in its counter, a condition variable makes you keep and check it yourself.

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.

Time runs down. Left: P calls sem_post(&full) with nobody waiting, full becomes 1; later C calls sem_wait(&full), full goes back to 0 and C goes on. Right: P calls cond_signal(&notEmpty) with nobody waiting and the signal is lost; later C calls cond_wait without checking count and sleeps forever although count is 1.
The post comes first: the semaphore keeps it as full = 1; the condition variable loses the signal, so only a check of count can save the consumer.

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 = 1 and calls cond_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 under m, 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

SemaphoreMutex + condition variable
What it holdsa count and sleeperssleepers only; the state is yours
Post / signal with nobody waitingkept (value + 1)lost
Spurious wake-upsno (only EINTR from a signal handler)yes: always wait in a while loop
Wake everyoneno: one post per sleepercond_broadcast
Condition"a unit is free", nothing elseany predicate on shared state
Ownernone: any thread may postthe mutex has one owner
Good forpools 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) with cond_wait(&notEmpty, &m) drops the memory: an early signal is lost (Demo 2).
  • if instead of while. After a spurious or stolen wake-up the thread goes on although the predicate is false and takes from an empty buffer.
  • Several sem_wait calls 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.