G27
Self-checkedTier 1 13 min reados.classic-sync-problems

Producer-consumer, readers-writers, dining philosophers

The standard synchronization problems and their semaphore solutions: the bounded buffer (producer-consumer), the readers-writers problem and its starvation, and the dining philosophers with the ways to avoid deadlock, each checked by exploring every interleaving.

Why it exists

A handful of problems capture the typical ways in which processes must coordinate, and they recur in textbooks, in system code and in GATE questions. Each is a small puzzle in using semaphores correctly: producer-consumer (the bounded buffer: processes that hand data to each other through a queue), readers-writers (many readers or one writer on shared data), and dining philosophers (processes that each need two shared resources at once). Learning their solutions teaches what can go wrong: lost updates, deadlock and starvation.

Prerequisites

You need wait, signal, the value arithmetic and the three uses of a semaphore from semaphores.

Precise definitions

Producer-consumer (bounded buffer)

A producer puts items into a buffer of NN slots and a consumer takes them out. Requirements: the producer must wait if the buffer is full, the consumer must wait if it is empty, and the buffer's data structure must be updated by one process at a time.

Three semaphores: mutex = 1 (exclusive access to the buffer), empty = N (number of free slots), full = 0 (number of items).

Producer:                      Consumer:
  repeat                         repeat
    produce an item                wait(full)
    wait(empty)                    wait(mutex)
    wait(mutex)                    remove an item
    insert the item                signal(mutex)
    signal(mutex)                  signal(empty)
    signal(full)                   consume the item
  forever                        forever

At every moment 0≤count≤N0 \le \text{count} \le N, and empty+full=N\text{empty} + \text{full} = N when nobody is inside (the semaphores are counting the slots). The order of the two waits matters: taking mutex before empty (or full) can deadlock. If the buffer is full, the producer holds the mutex and waits for empty; the consumer needs the mutex to make room: neither can proceed.

Readers-writers

A shared data set. Any number of readers may read at the same time; a writer needs exclusive access (no reader and no other writer).

First readers-writers problem (readers preference: no reader waits unless a writer already holds the data). Shared: rc = 0 (number of readers), mutex = 1 (protects rc), wrt = 1 (exclusive access for writers).

Writer:                        Reader:
  wait(wrt)                      wait(mutex); rc++;
  write                          if (rc == 1) wait(wrt);      // the first reader locks out writers
  signal(wrt)                    signal(mutex)
                                 read
                                 wait(mutex); rc--;
                                 if (rc == 0) signal(wrt);    // the last reader lets writers in
                                 signal(mutex)

Readers share, a writer is alone. But a writer can starve: while any reader is active, new readers keep entering (they only wait if a writer is already writing), so a waiting writer can be overtaken without bound.

Second readers-writers problem gives priority to writers: once a writer is waiting, no new reader may start. Now readers can starve. A fair solution (a third version) queues requests in arrival order, so neither starves, at some cost in concurrency.

Dining philosophers

Five philosophers sit at a round table with a fork between each pair (five forks). A philosopher thinks, then needs both the left and the right fork to eat, then puts them down. Each fork is a semaphore with value 1.

Naive solution (each takes the left fork, then the right):

Philosopher i:
  repeat
    think
    wait(fork[i]); wait(fork[(i + 1) % 5]);
    eat
    signal(fork[i]); signal(fork[(i + 1) % 5])
  forever

Neighbours never eat together, but it can deadlock: if all five take their left fork at the same moment, each holds one fork and waits for the next, which is held by the neighbour: a circular wait.

Deadlock-free fixes.

  • Limit the number at the table: allow at most n−1n - 1 philosophers to try to eat at once (a semaphore room initialised to n−1n - 1). With one fewer than forks, some philosopher can always take both.
  • Asymmetric / ordered pick-up: number the forks and always take the lower-numbered fork first (a similar fix: an odd philosopher takes the left fork first and an even one the right fork first; either way the circular wait is broken). This breaks the circular wait.
  • Pick up both forks atomically (inside a critical section, or only if both are free). A monitor-based solution does this.

Avoiding deadlock does not by itself avoid starvation: a philosopher could be bypassed for ever by unlucky timing; extra care (such as fairness in the waiting queues) is needed.

How it works

The animations are real schedules found by exploring every interleaving.

Producer-consumer with a buffer of 2 slots. The producer runs ahead until the buffer is full (empty = 0); it then blocks on wait(empty). The consumer's signal(empty) wakes it. At all times count is between 0 and 2.

Producer and consumer with a buffer of size 2: start

Interleaving found by search, not typed
Producer
▶wait(empty)
wait(mutex)
buffer[in] = item; count = count + 1
signal(mutex)
signal(full)
produce next item
Consumer
▶wait(full)
wait(mutex)
item = buffer[out]; count = count - 1
signal(mutex)
signal(empty)
consume item
count
0
mutex
1
empty
2
full
0

Step 0

count = 0 items in a buffer of 2 slots. empty = 2 (free slots), full = 0 (items), mutex = 1. The producer runs ahead until the buffer is full and then blocks on empty; the consumer's signals wake it.

0 / 25
computed by script: exhaustive search of the interleavings of a small concurrent program. python3 scripts/gen-os-sync-traces.py

The same buffer with the waits in the wrong order (mutex first). This is the shortest schedule that deadlocks: one thread holds mutex and waits for a slot or an item that the other can only provide by taking mutex.

The same buffer with wait(mutex) taken BEFORE wait(empty): start

Interleaving found by search, not typed
Producer
▶wait(mutex)
wait(empty)
buffer[in] = item; count = count + 1
signal(mutex)
signal(full)
produce next item
Consumer
▶wait(mutex)
wait(full)
item = buffer[out]; count = count - 1
signal(mutex)
signal(empty)
consume item
count
0
mutex
1
empty
2
full
0

Step 0

Only the order of the two waits is changed: each thread now takes mutex first. This is the shortest schedule that deadlocks.

0 / 3
computed by script: exhaustive search of the interleavings of a small concurrent program. python3 scripts/gen-os-sync-traces.py

Readers-writers (readers preference). Reader R1 is reading when the writer W arrives and blocks on wrt; reader R2 then arrives and starts reading although W has been waiting longer. Repeated for ever, W never gets in.

Readers-preference: a writer waits while new readers keep entering: start

Interleaving found by search, not typed
R1
▶wait(mutex)
rc = rc + 1
if (rc == 1)
wait(wrt)
signal(mutex)
reading
wait(mutex)
rc = rc - 1
if (rc == 0)
signal(wrt)
signal(mutex)
other work
R2
▶wait(mutex)
rc = rc + 1
if (rc == 1)
wait(wrt)
signal(mutex)
reading
wait(mutex)
rc = rc - 1
if (rc == 0)
signal(wrt)
signal(mutex)
other work
W
▶wait(wrt)
writing
signal(wrt)
other work
rc
0
mutex
1
wrt
1

Step 0

rc = 0 readers, mutex = 1, wrt = 1. The first reader to arrive locks out writers (wait(wrt)); the last reader to leave lets them in. Here reader R1 is reading when the writer W arrives and blocks; reader R2 arrives later and enters ahead of it.

0 / 12
computed by script: exhaustive search of the interleavings of a small concurrent program. python3 scripts/gen-os-sync-traces.py

Dining philosophers, left fork first. This is the shortest schedule that ends in deadlock: every philosopher holds one fork and is queued on the next.

Three dining philosophers, each takes the left fork first: start

Interleaving found by search, not typed
Ph0
▶think
wait(fork0)
wait(fork1)
eat
signal(fork0)
signal(fork1)
done eating
Ph1
▶think
wait(fork1)
wait(fork2)
eat
signal(fork1)
signal(fork2)
done eating
Ph2
▶think
wait(fork2)
wait(fork0)
eat
signal(fork2)
signal(fork0)
done eating
fork0
1
fork1
1
fork2
1

Step 0

Philosopher i needs forks i and i + 1 (mod 3), taken in that order: his left fork first, then the right. A fork is a semaphore with value 1. This is the shortest schedule that ends in deadlock.

0 / 9
computed by script: exhaustive search of the interleavings of a small concurrent program. python3 scripts/gen-os-sync-traces.py

Worked examples

EX 1A real bounded buffer

Easy

A program runs a producer thread and a consumer thread with POSIX threads and the three semaphores (named semaphores, which exist on Linux and macOS). The producer sends the numbers 1 to 1000 through a 4-slot buffer.

#define _POSIX_C_SOURCE 200809L
#include <fcntl.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <unistd.h>
 
#define SLOTS 4
#define ITEMS 1000
 
static int buffer[SLOTS];
static int in, out, count, max_count;
static sem_t *empty_s, *full_s, *mutex_s;
 
static sem_t *make(const char *tag, unsigned value) {
    char name[64];
    snprintf(name, sizeof name, "/gate_%s_%ld", tag, (long)getpid());
    sem_unlink(name);
    sem_t *s = sem_open(name, O_CREAT | O_EXCL, 0600, value);
    sem_unlink(name);                         /* the semaphore disappears when the program ends */
    return s;
}
 
static void *producer(void *arg) {
    (void)arg;
    for (int i = 1; i <= ITEMS; i++) {
        sem_wait(empty_s);                    /* wait for a free slot */
        sem_wait(mutex_s);                    /* exclusive access to the buffer */
        buffer[in] = i; in = (in + 1) % SLOTS;
        count++;
        if (count > max_count) max_count = count;
        sem_post(mutex_s);
        sem_post(full_s);                     /* one more item */
    }
    return NULL;
}
 
static void *consumer(void *arg) {
    long *sum = arg;
    for (int i = 1; i <= ITEMS; i++) {
        sem_wait(full_s);
        sem_wait(mutex_s);
        *sum += buffer[out]; out = (out + 1) % SLOTS;
        count--;
        sem_post(mutex_s);
        sem_post(empty_s);
    }
    return NULL;
}
 
int main(void) {
    empty_s = make("empty", SLOTS);
    full_s = make("full", 0);
    mutex_s = make("mutex", 1);
    if (empty_s == SEM_FAILED || full_s == SEM_FAILED || mutex_s == SEM_FAILED) { puts("cannot create semaphores"); return 1; }
    pthread_t p, c;
    long sum = 0;
    pthread_create(&p, NULL, producer, NULL);
    pthread_create(&c, NULL, consumer, &sum);
    pthread_join(p, NULL);
    pthread_join(c, NULL);
    printf("consumed %d items, sum %ld (expected %d)\n", ITEMS, sum, ITEMS * (ITEMS + 1) / 2);
    printf("buffer never held more than %d items: %d\n", SLOTS, max_count <= SLOTS);
    printf("items left in the buffer: %d\n", count);
    return 0;
}
consumed 1000 items, sum 500500 (expected 500500)
buffer never held more than 4 items: 1
items left in the buffer: 0

Reading it. Every item arrives exactly once (the sum is 1+2+⋯+1000=5005001 + 2 + \cdots + 1000 = 500500), the count never exceeds the 4 slots, and the buffer ends empty. The semaphores did their jobs: empty stopped the producer when the buffer was full, full stopped the consumer when it was empty, and mutex kept the updates of in, out and count from racing. A passing run does not prove correctness (timing varies), which is why the exhaustive search matters; it proved the same properties for all schedules of the model.

EX 2What the search proves about the buffer

Medium

The model of the buffer (1 producer and 1 consumer, 2 slots) has 72 reachable states; with 2 producers and 2 consumers it has 1504. In every one of them the count is 0, 1 or 2, no state is a deadlock, and no two threads are ever inside mutex. The same holds for buffer sizes up to 3 (checked by an independent model that spins instead of queueing). Swapping the order of the first two waits (mutex first) makes a deadlock reachable (79 states in the model): the animation shows the shortest schedule. So the order empty (or full) before mutex is not a style choice; it is what makes the solution correct.

EX 3Readers and writers, by hand

Medium

State: rc = 0, mutex = 1, wrt = 1. R1 arrives: rc = 1, it is the first reader, so wait(wrt) succeeds (wrt = 0). R2 arrives: rc = 2, not the first, so it does not touch wrt and reads at once, together with R1. A writer W arrives: wait(wrt) finds 0 and blocks. R1 leaves: rc = 1, not the last, so no signal. R3 arrives: rc = 2, reads. R2 leaves, R3 leaves: when rc reaches 0 the last one does signal(wrt) and W finally runs. If new readers always arrive before the old ones leave, rc never reaches 0 and W starves. The search confirms that a waiting writer can be bypassed by readers without any bound, and that no state has a reader and a writer active together.

EX 4Which dining-philosophers solutions are safe?

GATE level

The search over the model for 3, 4 and 5 philosophers gives the results:

VariantDeadlock reachable?Neighbours eat together?
left fork first (naive)yesno
the last philosopher takes the other fork first (ordered)nono
at most n−1n - 1 philosophers try at once (room semaphore)nono

So a deadlock needs every philosopher to hold exactly one fork. Limiting to n−1n - 1 breaks it by pigeonhole: with n−1n - 1 philosophers and nn forks at least one can take both. Ordering the forks breaks the circular wait: the philosopher who takes the "other" fork first would be waiting for the lowest-numbered fork, which no philosopher can hold while waiting for a higher one. Remember that neither variant guarantees against starvation by itself.

EX 5Spot the bug

GATE level

The producer does wait(mutex); wait(empty); ...; signal(mutex); signal(full): deadlock when the buffer is full, as shown. The consumer forgets signal(empty): after the buffer has been filled and emptied once, the producer blocks for ever because empty never recovers. The producer does signal(full) before inserting: the consumer may take an item that is not there yet (underflow). Both use the same semaphore for empty and full: the counts of free slots and of items are mixed up. A reader forgets wait(mutex) around rc++: two readers can lose an update of rc (both read 0, both store 1), so rc later reaches 0 while a reader is still reading and the last one to leave signals wrt: a writer enters together with a reader. In each case reason about what the semaphore counts and who changes it.

Cheat box

Cheat box

  • Bounded buffer: mutex = 1, empty = N, full = 0. Producer: wait(empty); wait(mutex); insert; signal(mutex); signal(full). Consumer: wait(full); wait(mutex); remove; signal(mutex); signal(empty). Resource semaphore before mutex.
  • Readers-writers (first): readers share, writer alone; first reader locks wrt, last reader releases; rc protected by mutex. Writers can starve (second problem: readers can starve).
  • Dining philosophers: naive left-then-right can deadlock (all hold one fork). Fixes: at most n−1n - 1 at the table; ordered/asymmetric pick-up; take both atomically.
  • Deadlock-free does not mean starvation-free.
  • Count invariant of the buffer: 0≤count≤N0 \le \text{count} \le N, empty+full=N\text{empty} + \text{full} = N when idle.

Traps and GATE patterns

Connections

The solutions use semaphores throughout; a monitor expresses the same patterns more safely. The deadlock in the naive philosophers and in the wrong-order buffer is the circular wait of deadlock conditions, and its cures are instances of what is generalized by deadlock avoidance. Starvation in readers-writers is the same phenomenon as the starvation of long jobs in CPU scheduling.

PYQs

4 previous year questions from your book. I re-solved each one myself (2 by running or simulating, 2 by reasoning only); the book's key is shown next to my answer. All PYQs

GATE 2018 · MCQQ9 · page 319 of the book
Consider the following solution to the producer-
consumer synchronization problem. The shared
buffer size is N. Three semaphores empty,
full and mutex are defined with respective initial
values of 0, N and 1. Semaphore empty denotes the
number of available slots in the buffer, for the
consumer to read from. Semaphore full denotes the
number of available slots in the buffer, for the
producer to write to. The placeholder variables,
denoted by P, Q, R and S, in the code below can be
assigned either empty or full. The valid semaphore
operations are: wait () and signal ().
Producer
Consumer
do{
do {
wait (P);
wait (R);
wait (mutex);
wait mutex);
//Add item to buffer
//Consume item from
buffer
Signal (mutex);
Signal (mutex);
Signal (Q);
Signal (S);
} while (1);
} while (1);

Which one of the following assignments to P, Q, R
and S will yield the correct solution?
  • (a)P: full, Q: full, R: empty, S: empty
  • (b)P: empty, Q: empty, R: full, S: full
  • (c)P: full, Q: empty, R: empty, S: full
  • (d)P: empty, Q: full, R: full, S: empty
GATE 2014 · MCQQ15 · page 320 of the book
Consider the procedure below for the Producer-
Consumer problem which uses semaphores:

semaphore n = 0;
semaphore s = 1;
void producer() void consumer()
{
    while(true) while(true)
    {
        produce();
        semWait(s) ;
        addToBuffer();
        semSignal(s);
        semSignal(n);
    }
}
void consumer()
{
    while(true)
    {
        semWait(s);
        semWait(n);
        removeFromBuffer();
        semSignal(s);
        consume();
    }
}

OPERATING SYSTEMS

Which one of the following is TRUE?
  • (a)The producer will be able to add an item to the buffer, but the consumer can never consume it.
  • (b)The consumer will remove no more than one item from the buffer.
  • (c)Deadlock occurs if the consumer succeeds in acquiring semaphore s when the buffer is empty.
  • (d)The starting value for the semaphore n must be 1 and not 0 for deadlock-free operation.
GATE 2006 · MCQ · 2 marksQ25 · page 324 of the book
Barrier is a synchronization construct where a set of
processes synchronize globally. Let the number of
processes be three and S be a binary semaphore.
Consider the following implementation:
void barrier(void) {
1:  P(S);
2:  process_arrived++;
3:  V(S);
4:  while (process_arrived != 3);
5:  P(S);
6:  process_left++;
7:  if (process_left == 3) {
8:      process_arrived = 0;
9:      process_left = 0;
10: }
11: V(S);
}
Variables process_arrived and process_left are
shared and initialized to 0.
Which one of the following is true?
  • (a)The barrier implementation is wrong due to the use of binary semaphore S
  • (b)The barrier implementation may lead to a deadlock if two barrier invocations are used in immediate succession
  • (c)Lines 6 to 10 need not be inside a critical section
  • (d)The barrier implementation is correct if there are only two processes instead of three
GATE 2006 · MCQQ26 · page 324 of the book
Barrier is a synchronization construct where a set of
processes synchronize globally. Let the number of
processes be three and S be a binary semaphore.
Consider the following implementation:

void barrier(void) {

1:  P(S);

2:  process_arrived++;

3:  V(S);

4:  while (process_arrived != 3);

5:  P(S);

6:  process_left++;

7:  if (process_left == 3) {

8:  process_arrived = 0;

9: process_left = 0;

10: }

11: V(S);

}

The variables process_arrived and process_left are
shared and initialized to zero.

Which one of the following rectifies the problem in
the implementation?
  • (a)Lines 6 to 10 are simply replaced by process_arrived--
  • (b)At the beginning of the barrier the first process to enter waits until process_arrived becomes zero before proceeding to execute P(S)
  • (c)Context switch is disabled at the beginning of the barrier and re-enabled at the end
  • (d)The variable process_left is made private instead of shared OPERATING SYSTEMS

Self-check

Question 1

A bounded buffer has 8 slots. What are the initial values of mutex, empty and full?

Show answer

mutex = 1, empty = 8, full = 0.

Question 2

In the first readers-writers solution, three readers are reading and a writer arrives. When can the writer start?

Show answer

Only after the last of the readers has left and signalled wrt (and while no new reader has taken wrt); new readers arriving before then can keep it waiting.

Question 3

Why does limiting the table to n−1n - 1 philosophers prevent deadlock?

Show answer

Deadlock needs every philosopher holding one fork and waiting for the next. With at most n−1n - 1 philosophers competing, at most n−1n - 1 forks can be held while one fork is free, and the philosopher next to the free fork can take it and eat.

Question 4

A consumer runs wait(mutex); wait(full); ... while the producer runs wait(empty); wait(mutex); .... Can it deadlock?

Show answer

Yes. When the buffer is empty the consumer holds mutex and waits for full; the producer cannot enter because it needs mutex, so nobody can produce.

Revision card

One-screen revision

  • Buffer: mutex = 1, empty = N, full = 0; resource semaphore before mutex.
  • Readers-writers: rc, mutex, wrt; first/last reader handle wrt; writer can starve.
  • Philosophers: left-then-right deadlocks; fixes: n−1n - 1 at the table, ordered forks, atomic pick-up.
  • Deadlock-free is not starvation-free.

References

  • ·Silberschatz, Galvin, Gagne, Operating System Concepts, Chapter 7 Synchronization Examples (classic problems of synchronization)
  • ·Tanenbaum and Bos, Modern Operating Systems, Chapter 2 Processes and Threads (classical IPC problems)
  • ·Stallings, Operating Systems: Internals and Design Principles, Chapter 5 Concurrency (producer/consumer, readers/writers)

Verification

All the safety, deadlock and starvation claims are established by exploring EVERY interleaving of the model programs (scripts/gen-os-sync-traces.py) and are cross-checked by independently written explicit models in scripts/enum-checks/os_sync.py: for the bounded buffer, that the item count stays between 0 and N, with one or two producers and consumers and any capacity up to 3, and that taking mutex before empty or full deadlocks; for the philosophers (3, 4 and 5 of them), that only the left-fork-first version can deadlock, that neighbours never eat together in any version, and that the 'at most n-1 in the room' and 'one philosopher picks up the other fork first' fixes are deadlock-free; for readers-writers, that readers can overlap, a writer is always alone, and that a waiting writer can be overtaken by an unbounded number of readers. A real run with POSIX threads and named semaphores (the C program on this page; on systems where semaphores live in a separate library it may need -pthread) shows 1000 items passing through a 4-slot buffer with the correct sum. Freedom from STARVATION of the fair solutions (and the writers-priority variants) is stated from the textbooks, not machine-checked. Textbook chapter titles are from memory. SECOND PASS (2026-11-02, done by the assistant, not a human check): every Self-check question was solved before the printed answer was read and all agreed; the definitions, worked examples, cheat box and traps were read claim by claim and the numbers re-derived by hand. One overstatement fixed (ordered fork pick-up is not 'equivalent' to the odd/even rule). Status raised to self-checked; verified needs a textbook or human check.

last verified: never