The producer/consumer (bounded-buffer) problem is the same one from the condition variables chapter โ now rebuilt on semaphores, which turns out to be remarkably clean.
31.4 The Producer/Consumer (Bounded Buffer) Problem
The buffer and its put/get (a ring, indices mod MAX):
int buffer[MAX];
int fill = 0, use = 0;
void put(int value) {
buffer[fill] = value; // Line F1
fill = (fill + 1) % MAX; // Line F2
}
int get() {
int tmp = buffer[use]; // Line G1
use = (use + 1) % MAX; // Line G2
return tmp;
}
First attempt: two semaphores, empty and full
Use two counting semaphores: empty counts free slots (init to
MAX), full counts ready items (init to 0). A producer waits for
a free slot then signals a full one; a consumer does the mirror image:
sem_t empty, full;
void *producer(void *arg) {
for (int i = 0; i < loops; i++) {
sem_wait(&empty); // P1: is there a free slot?
put(i); // P2
sem_post(&full); // P3: announce a new item
}
}
void *consumer(void *arg) {
for (int i = 0; i < loops; i++) {
sem_wait(&full); // C1: is there an item?
int tmp = get(); // C2
sem_post(&empty); // C3: announce a free slot
}
}
// main: sem_init(&empty, 0, MAX); sem_init(&full, 0, 0);
The two semaphores are perfect counters: empty starts at MAX (all
slots free), full at 0 (nothing to eat). Watch a full cycle with
MAX = 1:
Consumer runs first: sem_wait(full) drives full 0 โ โ1, so it blocks โ nothing to consume yet.
With MAX=1 (and even with multiple producers/consumers on the ordering alone) this works beautifully. Butโฆ
The bug at MAX > 1: a missing mutex
With MAX > 1 and multiple producers, put() itself races. empty and
full control how many may produce or consume โ but not mutual
exclusion over the shared buffer indices:
1Pa writes buffer[0] (Line F1)
Producer Pa runs put(): it writes its value A into buffer[fill], where fill is 0. It is ABOUT to increment fill (Line F2) โ but has not yet.
A tempting fix that deadlocks
The obvious repair: wrap everything in a binary-semaphore mutex. But
put it around the whole loop body โ including the empty/full waits
(Fig 31.11) โ and you get a deadlock deadlock A situation where a set of threads are each holding a resource and waiting for one held by another, forming a cycle in which none can proceed โ e.g. every dining philosopher holding their left fork and waiting for their right, or a consumer holding a mutex while blocked on 'full' while the producer that would post 'full' is blocked on that mutex. Studied in depth in ch32.
defined in ch. 31 โ open in glossary
:
// BROKEN โ mutex scope too wide
sem_wait(&mutex); // P0 / C0 โ acquired BEFORE the empty/full wait
sem_wait(&empty); // P1 (consumer: sem_wait(&full))
put(i); // P2
sem_post(&full); // P3
sem_post(&mutex); // P4 / C4
Figure 31.11โs deadlock: the consumer sleeps on full while HOLDING the mutex; the producer that would post full is stuck waiting for that mutex. A cycle โ neither can move.
The consumer grabs the mutex (C0), then blocks on sem_wait(&full)
(C1) with the lock still held. The producerโs very first move is
sem_wait(&mutex) (P0) โ already held โ so it blocks too. Each waits
for the other: a classic deadlock.
At last, a working solution
The fix is simply to shrink the lockโs scope: hold the mutex only
around put()/get(), leaving the empty/full waits outside it
(Fig 31.12):
// CORRECT โ mutex only around the critical section
sem_wait(&empty); // P1
sem_wait(&mutex); // P1.5 โ mutex INSIDE the empty/full wait
put(i); // P2
sem_post(&mutex); // P2.5
sem_post(&full); // P3
// consumer mirrors: wait(full) โ wait(mutex) โ get โ post(mutex) โ post(empty)
Now a thread can block on empty/full without holding the mutex, so
no cycle can form. The result โ two counting semaphores for ordering
plus a binary semaphore for mutual exclusion โ is the canonical bounded
buffer. Understand it now; use it later.
Check yourself: producer/consumer with semaphores
1.What do the empty and full semaphores count, and how are they initialized?
2.With MAX > 1 and multiple producers, put() races even though empty/full are used. Why?
3.Wrapping the whole loop body in a mutex (mutex around the empty/full waits too) deadlocks. Trace it.
4.How does the correct solution (Fig 31.12) avoid the deadlock?
5.MAX = 1, and the consumer happens to run first. What happens?