Operating System - Classical Synchronization Problems



There are some classical synchronization problems that are used to illustrate the challenges of process synchronization and inter-process communication (IPC) in an operating system. These problems help in understanding how to manage concurrent processes and ensure data consistency across multiple entities (processes or threads in the cases of OS)

Here are some of the most well-known classical synchronization problems −

Dining Philosophers Problem

In the Dining Philosophers Problem, there are certain number of philosophers sitting around a circular table. Each philosopher have two states, thinking and eating. Between each pair of philosophers, there is a single cutlery.

To start eating, a philosopher needs access to both the cutlery on their left and right. Our task is to design a protocol that ensures that no deadlock and no starvation in the system.

The image below illustrates the Dining Philosophers Problem −

Dining Philosophers Problem

Solution

To solve this problem, we can use semaphores to represent the cutleries. Each philosopher will follow these rules:

  • The cutleries are represented as binary semaphores initialized to 1 (available).
  • A philosopher must acquire both the left and right cutlery before eating.
  • After eating, the philosopher releases both the left and right cutlery.

The pseudo code for the philosopher's behavior can be represented as follows −

semaphore fork[3] = {1, 1, 1}; // Three forks
semaphore spoon[3] = {1, 1, 1}; // Three spoons

void philosopher(int i) {
    while (true) {
        think(); // Philosopher is thinking

        // Pick up fork and spoon
        wait(fork[i]);       // Pick up fork
        wait(spoon[i]);      // Pick up spoon

        eat(); // Philosopher is eating

        // Put down fork and spoon
        signal(spoon[i]);    // Put down spoon
        signal(fork[i]);     // Put down fork
    }
}

Producer Consumer Problem

The Producer Consumer Problem is also known as the Bounded Buffer Problem. It involves two types of processes, the producer process and the consumer process, which share a common, fixed-size buffer used as a queue.

The producer's job is to generate data, put it into the buffer, and start again. At the same time, the consumer is consuming the data (i.e., removing it from the buffer), one piece at a time.

The image below illustrates the Producer Consumer Problem −

Producer Consumer Problem

The task is to make sure that the producer won't try to add data into the buffer if it's full and that the consumer won't try to remove data from an empty buffer.

Solution

To solve this problem, we can use semaphores to keep track of the number of empty and full slots in the buffer. The empty semaphore counts the number of empty slots, while the full semaphore counts the number of full slots. A mutex is used to ensure mutual exclusion when accessing the buffer.

semaphore empty = N; // Number of empty slots in the buffer
semaphore full = 0;  // Number of full slots in the buffer
semaphore mutex = 1; // Mutex for critical section

The producer will have to follow these steps −

void producer() {
    while (true) {
        // Produce an item
        item = produce_item();
        
        wait(empty); // Decrement empty count
        wait(mutex); // Enter critical section
        
        // Add the item to the buffer
        add_item_to_buffer(item);
        
        signal(mutex); // Leave critical section
        signal(full);  // Increment full count
    }
}

The consumer will have to follow these steps −

void consumer() {
    while (true) {
        wait(full);  // Decrement full count
        wait(mutex); // Enter critical section
        
        // Remove an item from the buffer
        item = remove_item_from_buffer();
        
        signal(mutex); // Leave critical section
        signal(empty); // Increment empty count
        
        // Consume the item
        consume_item(item);
    }
}

Reader Writer Problem

In the Reader Writer Problem, there are two types of processes: readers and writers. Readers can read the data at the same time without any issues, but for writers, they need exclusive access to the data to prevent inconsistencies. Our task is to design a system that allows multiple readers to read simultaneously but when writers are writing, no other readers or writers can access the data.

Solution

To solve this problem, we again use semaphores. We will use a mutex to protect the count of readers and a semaphore to control access to the shared resource. The writeBlock semaphore will ensure that only one writer can access the resource at a time.

semaphore mutex = 1;       // Mutex for protecting read_count
semaphore writeBlock = 1;  // Semaphore for controlling access to the resource
int read_count = 0;        // Number of active readers

The pseudocode for the reader process is as follows −

void reader() {
    wait(mutex);
    read_count++;
    if (read_count == 1) {
        wait(writeBlock); // first reader locks out writers
    }
    signal(mutex);

    // critical section (reading)
    read_data();

    wait(mutex);
    read_count--;
    if (read_count == 0) {
        signal(writeBlock); // last reader releases writers
    }
    signal(mutex);
}

The pseudocode for the writer process is as follows −

void writer() {
    wait(writeBlock); // acquire exclusive access for writing

    // critical section (writing)
    write_data();

    signal(writeBlock); // release exclusive access
}

Sleeping Barber Problem

In the Sleeping Barber Problem, there is a barber shop with one barber, one barber chair, and a waiting room with N chairs.

  • When there is no customer, the barber sleeps in the barber chair.
  • When a customer arrives, they either wake up the barber if he is sleeping or sit in an empty chair in the waiting room if the barber is busy.
  • If there are no empty chairs in the waiting room, the customer will leave the shop.

The image below illustrates the Sleeping Barber Problem −

Sleeping Barber Problem Diagram

Solution

To solve this problem, we can use following semaphores −

  • waitingRoom − This semaphore keeps the count of available chairs in the waiting room.
  • barberChair − A binary semaphore to indicate whether the barber chair is occupied or not. It is initialized to 1 (available).
  • barberSleep − This also a binary semaphore used to put the barber to sleep when there are no customers. It is initialized to 0 (sleeping).
// Barber behavior
void barber() {
    while (true) {
        wait(barberSleep); // Sleep if there are no customers
        wait(barberChair); // Acquire the barber chair
        // Cut hair
        signal(barberChair); // Release the barber chair
    }
}

// Customer behavior
void customer() {
    if (wait(waitingRoom) == 0) { // Check for available chairs
        signal(waitingRoom); // Leave if no chairs are available
        return;
    }
    wait(barberChair); // Acquire the barber chair
    signal(barberSleep); // Wake up the barber if sleeping
    // Get hair cut
    signal(barberChair); // Release the barber chair
}

Conclusion

In real life, we often encounter situations where multiple entities need to access shared resources concurrently, such as waiting in queue for a service or sharing a common facility. Such situations can also occur in operating systems where multiple processes and threads are running simultaneously while sharing resources like memory, files, and devices. The classical synchronization problems discussed above will help us to understand how to deal with such situations without causing deadlocks and starvation.

Advertisements