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 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 , and 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 philosophers to try to eat at once (a semaphore
roominitialised to ). 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▶wait(empty)wait(mutex)buffer[in] = item; count = count + 1signal(mutex)signal(full)produce next item
▶wait(full)wait(mutex)item = buffer[out]; count = count - 1signal(mutex)signal(empty)consume item
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.
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▶wait(mutex)wait(empty)buffer[in] = item; count = count + 1signal(mutex)signal(full)produce next item
▶wait(mutex)wait(full)item = buffer[out]; count = count - 1signal(mutex)signal(empty)consume item
Step 0
Only the order of the two waits is changed: each thread now takes mutex first. This is the shortest schedule that deadlocks.
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▶wait(mutex)rc = rc + 1if (rc == 1)wait(wrt)signal(mutex)readingwait(mutex)rc = rc - 1if (rc == 0)signal(wrt)signal(mutex)other work
▶wait(mutex)rc = rc + 1if (rc == 1)wait(wrt)signal(mutex)readingwait(mutex)rc = rc - 1if (rc == 0)signal(wrt)signal(mutex)other work
▶wait(wrt)writingsignal(wrt)other work
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.
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▶thinkwait(fork0)wait(fork1)eatsignal(fork0)signal(fork1)done eating
▶thinkwait(fork1)wait(fork2)eatsignal(fork1)signal(fork2)done eating
▶thinkwait(fork2)wait(fork0)eatsignal(fork2)signal(fork0)done eating
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.
Worked examples
EX 1A real bounded buffer
EasyA 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: 0Reading it. Every item arrives exactly once (the sum is ), 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
MediumThe 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
MediumState: 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 levelThe search over the model for 3, 4 and 5 philosophers gives the results:
| Variant | Deadlock reachable? | Neighbours eat together? |
|---|---|---|
| left fork first (naive) | yes | no |
| the last philosopher takes the other fork first (ordered) | no | no |
| at most philosophers try at once (room semaphore) | no | no |
So a deadlock needs every philosopher to hold exactly one fork. Limiting to breaks it by pigeonhole: with philosophers and 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 levelThe 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;rcprotected bymutex. Writers can starve (second problem: readers can starve). - Dining philosophers: naive left-then-right can deadlock (all hold one fork). Fixes: at most at the table; ordered/asymmetric pick-up; take both atomically.
- Deadlock-free does not mean starvation-free.
- Count invariant of the buffer: , when idle.
Traps and GATE patterns
Connections
Builds on
Across subjects
No cross-subject links yet.
Within this subject
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
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
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.
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
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 answerHide
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 answerHide
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 philosophers prevent deadlock?
Show answerHide
Deadlock needs every philosopher holding one fork and waiting for the next. With at most philosophers competing, at most 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 answerHide
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 handlewrt; writer can starve. - Philosophers: left-then-right deadlocks; fixes: 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
My notes (saved in this browser)