Introduction

Three classic problems (Silberschatz 7.1) are the standard test bed for any new synchronization primitive. The bounded-buffer (producer-consumer) problem is about counting and waiting for space or data. The readers-writers problem is about letting many readers share while writers get exclusive access, and about who gets starved. The dining philosophers problem is about acquiring several resources at once without deadlock or starvation.

The standard semaphore solution of each problem works for the reasons given in its section; small changes break it, such as swapping two wait calls or letting every philosopher pick up the same side first, and the readers-writers section adds the priority variants.

Prerequisites

The bounded-buffer problem

Producers put items into a buffer of nn slots; consumers take them out. A producer must wait when the buffer is full, a consumer when it is empty, and buffer updates must be mutually exclusive.

Before starting any producer or consumer, require n >= 1, allocate buffer[0..n-1], and initialize in <- 0 and out <- 0; retain the semaphore initial values below.

Text
semaphores: mutex <- 1     (protects the buffer and its indices)
            empty <- n     (free slots)
            full  <- 0     (filled slots)

PRODUCER:                         CONSUMER:
loop                              loop
  produce item                      wait(full)
  wait(empty)                       wait(mutex)
  wait(mutex)                       item <- buffer[out]; out <- (out + 1) mod n
  buffer[in] <- item                signal(mutex)
  in <- (in + 1) mod n              signal(empty)
  signal(mutex)                     consume item
  signal(full)

Invariant. At every moment, empty + full equals nn minus the number of producers and consumers between their first wait and their final signal. When nobody is in the middle of an operation, empty + full =n= n and full is the number of items. All nn slots can be used, unlike the busy-waiting in/out buffer of Inter-Process Communication, which can fill only n−1n - 1.

Bounded buffer with three semaphores
Figure 1. A five-slot bounded buffer after three items are produced and none consumed: slots 0 to 2 full, in = 3, out = 0, and the semaphores at mutex = 1, empty = 2, full = 3.

IMPERATIVE RULE. Wait on the counting semaphore (empty or full) before mutex, never after. If a producer runs wait(mutex) first and then blocks on wait(empty) because the buffer is full, it sleeps while holding mutex; no consumer can ever enter to make space, and the system deadlocks. The order of the two signal calls does not matter for correctness.

With several producers and consumers the same code works unchanged, because mutex serializes all index updates.

The readers-writers problem

Many processes read a shared object; some write it. Any number of readers may read together, but a writer needs exclusive access (no other writer, no reader).

First readers-writers solution (readers preference)

Text
semaphores: rw_mutex <- 1    (held by a writer, or by the group of readers)
            mutex <- 1       (protects read_count)
integer:    read_count <- 0

WRITER:                         READER:
wait(rw_mutex)                  wait(mutex)
  write                         read_count <- read_count + 1
signal(rw_mutex)                if read_count = 1 then wait(rw_mutex)   -- first reader locks writers out
                                signal(mutex)
                                  read
                                wait(mutex)
                                read_count <- read_count - 1
                                if read_count = 0 then signal(rw_mutex) -- last reader lets writers in
                                signal(mutex)
  • The first reader to arrive locks rw_mutex on behalf of all readers; the last to leave releases it.
  • If a writer holds rw_mutex, the first reader blocks on it while holding mutex, so all later readers queue on mutex. When the writer leaves, they all go in.
  • Writers can starve. As long as readers keep arriving while at least one reader is inside, read_count never returns to 0 and a waiting writer never gets rw_mutex.

Second readers-writers problem (writers preference)

Once a writer is waiting, no new reader may start. This needs an extra semaphore that readers must pass (often called read_try) and a writer count; the first waiting writer locks readers out. Now readers can starve under a stream of writers. A fair solution queues readers and writers in arrival order.

VariantWho waits for whomWho can starve
First (readers preference)A writer waits until no reader is activeWriters
Second (writers preference)New readers wait while any writer waitsReaders
Fair (FIFO queue)Arrival orderNobody

Readers-writers locks. Many systems provide them directly (POSIX pthread_rwlock_t). They pay off when reads far outnumber writes; otherwise the bookkeeping costs more than a plain mutex.

The dining philosophers problem

Five philosophers sit round a table with one chopstick between each pair (five in all). A philosopher alternates thinking and eating, and needs both neighbouring chopsticks to eat.

Text
semaphores: chopstick[0..4] <- 1
PHILOSOPHER i:
loop
  think
  wait(chopstick[i])               -- left
  wait(chopstick[(i + 1) mod 5])   -- right
  eat
  signal(chopstick[(i + 1) mod 5])
  signal(chopstick[i])

Deadlock. If all five pick up their left chopstick at once, every right chopstick is taken and all five wait forever. All four Coffman conditions hold: chopsticks are exclusive, each holds one and waits for one, none can be taken away, and the waits form a cycle.

Remedies (each breaks one condition):

RemedyHowCondition broken
At most four at the tableA counting semaphore room <- 4 taken before the chopsticksCircular wait (one philosopher always gets both)
Asymmetric orderOdd philosophers take left first, even take right first (or just one philosopher reverses)Circular wait
Both or nonePick up both chopsticks inside a critical section, or with a monitor that checks both neighboursHold and wait

Deadlock-free is not starvation-free. In the monitor solution (a philosopher eats only if neither neighbour is eating), two neighbours can alternate so that the philosopher between them never finds both free. Preventing starvation needs extra fairness, such as a queue or priorities.

How many can eat at once? With five chopsticks and two per meal, at most ⌊5/2⌋=2\lfloor 5/2 \rfloor = 2 philosophers eat simultaneously, and they cannot be neighbours.

Worked examples

Which order deadlocks the bounded buffer?

With n=1n = 1, one producer making 3 items and one consumer taking 3: the standard order (counting semaphore first) never deadlocks and never puts the item count outside 0 to 1. Swapping the producer's two waits allows this sequence: the producer fills the slot, loops, takes mutex, blocks on empty (still 0); the consumer blocks on mutex. Deadlock. The same check with n=2n = 2, four items, or with two producers and two consumers, finds no error in the standard order.

Readers and writers

With two readers and one writer under the first solution: both readers can be reading at the same time, a reader and the writer are never active together, and two writers are never active together. Suppose reader A is reading, so read_count =1= 1 and rw_mutex =0= 0, and a writer is blocked on rw_mutex. A new reader B passes mutex, makes read_count =2= 2, skips wait(rw_mutex) and starts reading. If readers keep overlapping like this, the writer never runs.

Philosophers

For 3, 4 and 5 philosophers, the plain left-then-right program can deadlock, while the asymmetric version (the last philosopher takes right first) and the version with a room semaphore of n−1n - 1 cannot.

Semaphore values along one run

A five-slot buffer (n=5n = 5) starts empty. The producer tries to put in 7 items before the consumer starts; then the consumer takes 3 items while the producer finishes. Blocking semaphores are used, so a wait decrements first and a negative value counts the blocked processes. The mutex operations always succeed here and are left out.

StepEventemptyfullItems in buffer
1 to 5Producer adds items 1 to 5055
6Producer wait(empty) for item 6: blocks−1-155
7Consumer wait(full), takes item 1, signal(empty) wakes the producer044
8Producer stores item 6, signal(full)055
9Consumer takes item 2144
10Producer wait(empty), stores item 7, signal(full)055
11Consumer takes item 3144

At the end, 7 items were produced and 3 consumed, so full =7−3=4= 7 - 3 = 4 and empty =5−4=1= 5 - 4 = 1, whatever the interleaving, as the invariant requires once nobody is inside an operation. The −1-1 at step 6 appears only with blocking semaphores; with the busy-waiting definition the producer spins at empty = 0 and the value never goes negative.

Important properties and observations

  • The bounded-buffer solution uses each kind of semaphore pattern once: mutex for exclusion, empty and full for counting and waiting.
  • In readers-writers, rw_mutex is acquired by one reader and may be released by another (the last one): this is exactly why it must be a semaphore, not a mutex with ownership.
  • Every dining-philosophers fix works by breaking one of the four deadlock conditions; see Deadlock.
  • Starvation analysis is separate from deadlock analysis in all three problems.

Common misconceptions.

  • "Swapping the two signal calls in the producer causes deadlock." Only the order of the wait calls matters.
  • "The first readers-writers solution is starvation-free." Writers can starve.
  • "Limiting the table to four philosophers prevents starvation." It prevents deadlock; fairness depends on the semaphore's wake-up order.

GATE examination perspective

  • Spot the deadlock. In producer-consumer variants, look for a wait(mutex) followed by a wait on a counting semaphore that can be 0.
  • Initial values. empty =n= n, full =0= 0, mutex =1= 1; swapped initial values block everybody or allow overflow.
  • Readers-writers. Which process locks and unlocks rw_mutex (first and last reader); which side starves in each variant.
  • Dining philosophers. Deadlock in the symmetric program; fixes by asymmetry, by n−1n - 1 seats, or by taking both at once; maximum simultaneous eaters ⌊n/2⌋\lfloor n/2 \rfloor.
  • Counting values. Given a sequence of operations, track empty, full and the number of items; the sum empty + full returns to nn when no operation is in progress.

Summary

Sources

  1. A. Silberschatz, P. B. Galvin, G. Gagne, Operating System Concepts, 10th ed., Wiley: Chapter 7, Section 7.1 Classic Problems of Synchronization (bounded buffer, readers-writers, dining philosophers).
  2. A. S. Tanenbaum, H. Bos, Modern Operating Systems, 4th ed., Pearson: Section 2.3.5 Semaphores (producer-consumer with semaphores), Section 2.5 Classical IPC Problems (2.5.1 The Dining Philosophers Problem, 2.5.2 The Readers and Writers Problem).

Next steps