Facebook Pixel

Classical Synchronization Problems in Operating Systems

Process synchronization is one of the trickier aspects of OS design. When multiple processes run at the same time and share resources, things can go wrong quickly.

Over the years, researchers have come up with a handful of classic problems that capture these challenges well. Each one maps directly to real situations in operating systems where processes compete for shared resources like memory, files, and devices.

Dining Philosophers Problem

Picture a round table with philosophers seated around it. Each philosopher spends their time either thinking or eating. Between every two adjacent philosophers sits a single piece of cutlery. To eat, a philosopher needs both the cutlery to their left and the one to their right.

The challenge: how do we make sure no philosopher waits forever (starvation) and that the system never reaches a standstill where everyone is waiting on everyone else (deadlock)?

Dining Philosophers Problem Diagram

Dining Philosophers Problem: philosophers around a table with shared cutlery between them.

The Solution

Each piece of cutlery is modeled as a binary semaphore set to 1. A philosopher grabs both cutleries before eating and puts them back once done.

c
semaphore fork[3] = {1, 1, 1};
semaphore spoon[3] = {1, 1, 1};

void philosopher(int i) {
    while (true) {
        think();
        wait(fork[i]);         // Pick up left cutlery
        wait(spoon[i]);        // Pick up right cutlery
        eat();                 // Critical section
        signal(spoon[i]);      // Put down right cutlery
        signal(fork[i]);       // Put down left cutlery
    }
}
The Deadlock Risk
If every philosopher picks up their left cutlery at the same time, all of them are now holding one and waiting for the other. Nobody can eat, nobody puts anything down. This is a textbook circular wait deadlock. Common fixes include: allowing at most N-1 philosophers to sit at the table at once, requiring an odd-numbered philosopher to pick up the right cutlery first (breaking the cycle), or using a central arbitrator that grants permission to pick up both cutleries atomically.

Producer Consumer Problem

Also called the Bounded Buffer Problem, this one involves two kinds of processes sharing a fixed-size buffer. The producer keeps generating data and pushing it into the buffer, while the consumer pulls data out and processes it.

The tricky part: the producer should not add to a full buffer, and the consumer should not try reading from an empty one.

Producer Consumer Problem Diagram

Producer Consumer Problem: producers adding items to a bounded buffer, consumers removing them.

The Solution

Three semaphores do the job. One tracks empty slots, another tracks filled slots, and a mutex handles mutual exclusion during buffer access.

c
semaphore empty = N;     // Tracks empty slots
semaphore full = 0;      // Tracks filled slots
semaphore mutex = 1;     // Protects buffer access

void producer() {
    while (true) {
        item = produce_item();
        wait(empty);         // Wait if buffer is full
        wait(mutex);         // Enter critical section
        add_item_to_buffer(item);
        signal(mutex);       // Exit critical section
        signal(full);        // Signal: one more item available
    }
}

void consumer() {
    while (true) {
        wait(full);          // Wait if buffer is empty
        wait(mutex);         // Enter critical section
        item = remove_item_from_buffer();
        signal(mutex);       // Exit critical section
        signal(empty);       // Signal: one more slot free
        consume_item(item);
    }
}

Reader Writer Problem

Here we have two groups of processes: readers and writers. Multiple readers can safely access shared data at the same time since they are not modifying anything. Writers, though, need full exclusive access because any overlap with other readers or writers can corrupt the data.

The Solution

A counter keeps track of active readers. The first reader to arrive blocks writers from entering, and the last reader to leave lets writers back in. Writers simply wait for a clear path and proceed once they get it.

c
semaphore mutex = 1;       // Protects read_count
semaphore writeBlock = 1;  // Controls writer access
int read_count = 0;        // Active readers

void reader() {
    wait(mutex);
    read_count++;
    if (read_count == 1)
        wait(writeBlock);  // First reader blocks writers
    signal(mutex);

    read_data();           // Multiple readers can be here

    wait(mutex);
    read_count--;
    if (read_count == 0)
        signal(writeBlock); // Last reader unblocks writers
    signal(mutex);
}

void writer() {
    wait(writeBlock);      // Exclusive access
    write_data();
    signal(writeBlock);
}
Reader Preference vs Writer Preference
The solution above uses reader preference: new readers can keep entering even while a writer is waiting, which can starve writers. Writer preference flips this by blocking new readers once a writer is waiting. Neither is universally better. The right choice depends on your workload: read-heavy systems benefit from reader preference, write-critical systems need writer preference.

Sleeping Barber Problem

This one is set in a barbershop with one barber, one barber chair, and a waiting room with N seats. When no customers are around, the barber naps in their chair. A new customer either wakes the barber up or takes a seat in the waiting room if the barber is busy. If all seats are taken, the customer simply walks away.

Sleeping Barber Problem Diagram

Sleeping Barber Problem: barber, barber chair, and a waiting room with limited seats.

The Solution

Three semaphores are used. One tracks available waiting room chairs, another manages access to the barber chair, and a third handles the barber's sleep state.

c
semaphore barberSleep = 0;    // Barber starts asleep
semaphore barberChair = 1;   // One barber chair
semaphore waitingRoom = N;   // N seats in waiting room

void barber() {
    while (true) {
        wait(barberSleep);   // Sleep until a customer arrives
        wait(barberChair);   // Occupy the barber chair
        // --- Cut hair ---
        signal(barberChair); // Free the barber chair
    }
}

void customer() {
    if (tryWait(waitingRoom) == false) {
        return;              // No seats, leave the shop
    }
    signal(waitingRoom);     // Leave waiting room
    wait(barberChair);       // Sit in barber chair
    signal(barberSleep);     // Wake up the barber
    // --- Get haircut ---
    signal(barberChair);     // Free the barber chair
}

How They Map to Real Systems

Classical ProblemReal-World EquivalentCore Challenge
Dining PhilosophersProcesses competing for multiple shared resources (e.g., two I/O devices needed simultaneously)Deadlock and starvation avoidance
Producer ConsumerWeb servers queuing requests, print spoolers, logging pipelinesBuffer overflow/underflow prevention
Reader WriterDatabase queries vs updates, config file reads vs admin writesMaximizing read parallelism while protecting writes
Sleeping BarberThread pools with limited worker threads, network servers with connection limitsManaging a finite service capacity without dropping clients unnecessarily

These four problems might seem like abstract scenarios, but they map directly to real situations in operating systems where processes compete for shared resources. Getting a handle on these problems builds a solid foundation for writing concurrent code that avoids deadlocks and keeps every process moving forward.

Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.