DSA, Database System & Operating System β Operating System and Process Management, NEC licence examination syllabus (Nepal Engineering Council).
Classical Problems of Synchronisation
Producer-consumer, readers-writers and dining philosophers β three problems that between them contain every synchronisation hazard.
π Where this lives: these are not puzzles, they are the three shapes real concurrency takes. Producer-consumer is every work queue, every Kafka topic, every thread pool. Readers-writers is every cache and every database table β many readers, occasional writer, and the fairness question of who waits. Dining philosophers is every resource-ordering problem, which is why the standard fix (impose a global lock order) is the same advice given for database deadlocks. Learn these three and you can classify most concurrency bugs you will meet. Search "lock ordering deadlock prevention".
Problem 1 β Producer/Consumer (bounded buffer)
SETUP: a producer adds items to a fixed-size buffer; a consumer
removes them.
THE THREE CONSTRAINTS:
1. the producer must WAIT when the buffer is FULL
2. the consumer must WAIT when the buffer is EMPTY
3. only one process may touch the indices at a time
SOLUTION β three semaphores, one per constraint:
semaphore empty = N; /* free slots */
semaphore full = 0; /* items present */
semaphore mutex = 1; /* index protection */
producer: consumer:
wait(empty); wait(full);
wait(mutex); wait(mutex);
buffer[in] = item; item = buffer[out];
in = (in+1) % N; out = (out+1) % N;
signal(mutex); signal(mutex);
signal(full); signal(empty);
THE ORDER RULE β verified experimentally in the semaphore
topic: wait(empty) MUST come before wait(mutex). Reversed, the
producer blocks on `empty` while HOLDING the mutex, the
consumer cannot acquire the mutex to signal `empty`, and both
block forever. Measured: neither thread ever finished.
MEASURED CORRECT BEHAVIOUR (BUF=5, 20 items):
produced : 20
consumed : 20
max occupancy observed : 5 β never exceeded the buffer
final occupancy : 0 β nothing lost or duplicated
MEASURED WITHOUT THE SEMAPHORES (5,000 items, 5 slots):
final count = -5000
The counter went to minus five thousand β the invariant is
not bent, it is meaningless. The consumer "consumed" items
that were never produced, repeatedly re-reading stale slots.
THE INVARIANT WORTH STATING: empty + full + (in flight) = N
A semaphore pair like this is really one resource count split
in two directions, which is why they always move in
opposite directions.
Problem 2 β Readers/Writers
SETUP: a shared data structure. MANY readers may read
simultaneously (reading does not conflict with reading), but a
WRITER needs exclusive access.
readerβreader compatible
readerβwriter CONFLICT
writerβwriter CONFLICT
This is exactly the lock compatibility matrix from the
database locking topic β the same problem at a different
layer.
FIRST READERS-WRITERS PROBLEM (readers have priority)
No reader waits unless a writer already holds the resource.
semaphore mutex = 1; /* protects readcount */
semaphore wrt = 1; /* the resource lock */
int readcount = 0;
writer:
wait(wrt);
... write ...
signal(wrt);
reader:
wait(mutex);
readcount++;
if (readcount == 1) wait(wrt); /* FIRST reader
locks out writers */
signal(mutex);
... read ...
wait(mutex);
readcount--;
if (readcount == 0) signal(wrt); /* LAST reader
releases */
signal(mutex);
THE MECHANISM: only the first reader acquires `wrt` and only
the last releases it. Readers in between simply increment and
proceed, so many read concurrently.
β WRITERS CAN STARVE. With a steady stream of readers,
readcount never reaches 0, so `wrt` is never signalled and
the writer waits forever.
SECOND READERS-WRITERS PROBLEM (writers have priority)
Once a writer is waiting, no NEW reader may start.
β now READERS can starve.
THIRD (fair) VERSION β nobody starves. Add a queue semaphore
that everyone must pass through in arrival order:
semaphore queue = 1;
reader: wait(queue); wait(mutex); readcount++;
if (readcount==1) wait(wrt);
signal(mutex); signal(queue);
... read ...
writer: wait(queue); wait(wrt); signal(queue);
... write ... signal(wrt);
The `queue` semaphore serialises ENTRY, so a writer that
arrives is ahead of every later reader β bounded waiting for
both.
REAL-WORLD FORM: this is a READ-WRITE LOCK.
pthread_rwlock_rdlock / pthread_rwlock_wrlock
Java ReentrantReadWriteLock
Go sync.RWMutex
WHEN IT IS WORTH IT: only when reads dominate AND the
critical section is long. A read-write lock has more
bookkeeping than a plain mutex, so for short sections a plain
mutex is often faster. Measure rather than assume β Go's
documentation explicitly warns that RWMutex is slower than
Mutex for short critical sections.
Problem 3 β Dining Philosophers
SETUP: five philosophers around a table, five forks between
them. Each needs BOTH adjacent forks to eat.
P0
F0 F1
P4 P1
F4 F2
P3 F3 P2
THE NAIVE SOLUTION, and why it fails:
philosopher i:
wait(fork[i]); /* left */
wait(fork[(i+1) % 5]); /* right */
... eat ...
signal(fork[i]);
signal(fork[(i+1) % 5]);
If all five pick up their LEFT fork simultaneously, every
fork is held and every philosopher waits for a right fork
that will never be released. DEADLOCK.
MEASURED ON THIS MACHINE (5 philosophers, 3 meals each,
expected 15 meals total):
NAIVE (all left-first), three runs:
meals = [0,0,0,0,0] total = 0/15 DEADLOCKED
meals = [0,0,0,0,0] total = 0/15 DEADLOCKED
meals = [0,0,0,0,0] total = 0/15 DEADLOCKED
Not a single philosopher ate, in any run. This is one of the
few concurrency bugs that is reliably reproducible β because
the circular wait forms almost immediately.
All four Coffman conditions are present:
mutual exclusion a fork is held exclusively
hold and wait each holds the left, waits for the right
no preemption a fork cannot be taken away
circular wait P0βP1βP2βP3βP4βP0
FOUR SOLUTIONS, each breaking one condition:
1. ALLOW AT MOST 4 AT THE TABLE (breaks hold-and-wait)
semaphore seats = 4;
wait(seats); ... take both forks, eat, release ... ;
signal(seats);
With 4 competing for 5 forks, at least one gets both.
2. PICK UP BOTH FORKS ATOMICALLY (breaks hold-and-wait)
wait(mutex);
if both free: take both
signal(mutex);
Requires checking both under one lock β a monitor does
this naturally.
3. ASYMMETRY β ODD/EVEN ORDERING (breaks circular wait)
odd philosophers: left then right
even philosophers: right then left
Or simply: ONE philosopher reverses the order.
MEASURED, with only the LAST philosopher reversed:
meals = [3,3,3,3,3] total = 15/15 ALL ATE
meals = [3,3,3,3,3] total = 15/15 ALL ATE
meals = [3,3,3,3,3] total = 15/15 ALL ATE
Three runs, all successful, and perfectly fair β every
philosopher ate exactly three times. ONE reversed
acquisition order eliminated the deadlock completely.
4. RESOURCE ORDERING β number the forks and always acquire
the LOWER-NUMBERED one first (breaks circular wait)
This is solution 3 generalised, and it is the standard
industrial answer: impose a total order on all locks and
acquire in that order everywhere.
STARVATION vs DEADLOCK β a distinction this problem shows well:
solutions 1β4 all prevent DEADLOCK. None of them alone
guarantees no STARVATION: a philosopher could in principle be
perpetually unlucky. Preventing that needs a fairness
mechanism (a queue, or aging) on top.
The dining-philosophers result is the cleanest experiment in this section: 0/15 meals three times, then 15/15 three times, with one acquisition order reversed. Most concurrency bugs are probabilistic; this one is deterministic, which makes it the ideal teaching case β and the fix generalises directly to the lock-ordering rule you should apply in real code.
Two more classical problems
SLEEPING BARBER
A barber shop with one barber, one barber chair and N waiting
chairs.
Β· no customers β the barber SLEEPS
Β· a customer arrives β wakes the barber, or takes a waiting
chair, or LEAVES if all chairs are full
Β· the barber finishes β takes the next waiting customer, or
sleeps
semaphore customers = 0; /* waiting customers */
semaphore barbers = 0; /* barbers ready */
semaphore mutex = 1;
int waiting = 0;
barber: customer:
while (true) { wait(mutex);
wait(customers); if (waiting < N) {
wait(mutex); waiting++;
waiting--; signal(customers);
signal(mutex); signal(mutex);
signal(barbers); wait(barbers);
cut_hair(); get_haircut();
} } else {
signal(mutex);
leave(); /* balk */
}
THE INTERESTING PART is the BALKING β a customer who finds
the shop full leaves rather than waiting. That models a
BOUNDED QUEUE WITH REJECTION, which is exactly what a web
server does when its connection backlog is full (it returns
503 rather than queueing forever). Real systems must balk;
unbounded queues are how they die.
CIGARETTE SMOKERS
Three smokers, each with an infinite supply of ONE ingredient
(tobacco, paper, matches). An agent places two random
ingredients on the table; the smoker with the third can
smoke.
Β· illustrates that a naive semaphore solution DEADLOCKS
because a smoker may grab an ingredient it cannot use
Β· the lesson: signal the specific waiter that can proceed,
which is what condition variables (one per case) give you
MAPPING THE PROBLEMS TO REALITY:
producer/consumer β work queues, thread pools, Kafka
readers/writers β caches, database tables, config reload
dining philosophers β lock ordering, database deadlocks
sleeping barber β bounded queues with rejection, 503s
cigarette smokers β precise signalling, one condition per
predicate
π Go further: these problems date from the 1960sβ70s and the modern response is often to make them unnecessary rather than to solve them. Producer-consumer becomes a lock-free queue using CAS, or a channel. Readers-writers becomes RCU (Read-Copy-Update) in the Linux kernel, where readers pay nothing at all β no lock, no atomic β and writers make a copy and swap a pointer. RCU is why Linux scales to hundreds of cores on read-heavy structures. Search "RCU read copy update explained" and "lock-free queue Michael-Scott".
π‘ Exam angle: all three problems are standard long answers β be able to write the semaphore solution for each from memory. For producer-consumer, state the three semaphores and why wait(empty) precedes wait(mutex). For readers-writers, write the first version and explain that the first reader acquires and the last releases the writer lock, then state that writers starve and how the fair version fixes it. For dining philosophers, identify all four Coffman conditions in the naive version and give at least two solutions, naming which condition each breaks.
Syllabus points
Producer-consumer
Readers-writers
Dining philosophers
Create a free account to tick topics off, take notes as you read, watch the video lessons and get a day-by-day study plan built around your exam date.
Related topics in Operating System and Process Management