ยง31.4The Producer/Consumer (Bounded Buffer) Problem

Part II OSTEP pp. 372โ€“375 ยท ~6 min read

  • deadlock

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:

MAX = 1, one producer + one consumer on one CPU. Track empty (free slots) and full (ready items).
t =
Producer
Consumer
semaphores
empty 1 ยท full โˆ’1
tick 1 / 5 ยท one scheduling step per tickrunningreadysleeping

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:

Why put()/get() still need a lock: two producers racing on the SAME buffer slot (MAX > 1).
Abuffer[0]ยทbuffer[1]ยทbuffer[2]โ–ผ fill = 0

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.

step 1 / 3

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 :

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

Consumerholds mutex ๐Ÿ”’blocked on wait(full)Producerwould post(full) โ€” but firstโ€ฆblocked on wait(mutex)needs the producer to post fullneeds the consumer to release mutex

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?

5 questions