Operating System - Dining Philosophers Problem



The Dining Philosophers Problem is a classic synchronization problem that shows the challenges of sharing resources between multiple processes or entities. It was formulated by Edsger Dijkstra in 1965. Read this chapter to understand the problem, it's conditions and its solution.

Dining Philosophers Problem Explained

Imagine a six philosophers sitting around a circular table. Each philosopher has two states, thinking and eating. To start eating, a philosopher needs both fork and spoon simultaneously. However, there are only three forks and three spoons available on the table, which are placed between the philosophers as shown in the figure below −

Dining Philosophers Problem

A philosopher can only use the fork and spoon that are adjacent to them. Our challenge is to design a protocol that allows the philosophers to eat without facing deadlock or starvation.

Deadlock Situation in Dining Philosophers Problem

Now, imagine all the philosophers starts eating at the same time and each picks up the cutlery on their left first. This will lead to a deadlock situation. Each philosopher will be holding one piece of cutlery (either fork or spoon) and waiting for the other piece to become available. Result is that none of them can eat.

P1 -> Picks Spoon on left  ->  Needs Fork on right.(held by P6)
P2 -> Picks Fork on left   ->  Needs Spoon on right.(held by P1)
P3 -> Picks Spoon on left  ->  Needs Fork on right.(held by P2)
P4 -> Picks Fork on left   ->  Needs Spoon on right.(held by P3)
P5 -> Picks Spoon on left  ->  Needs Fork on right.(held by P4)
P6 -> Picks Fork on left   ->  Needs Spoon on right.(held by P5)

Result: Deadlock - No one can eat.

Let's see how we can solve this problem using the concept of semaphores.

Dining Philosophers Solution Using Semaphores

Semaphores are synchronization primitives that can be used to control access to shared resources. In this case, we can use semaphores to manage the availability of forks and spoons.

Algorithm

  • Both forks and spoons are represented as binary semaphores initialized to 1 (available).
  • A philosopher must acquire both the fork and spoon before eating.
  • After eating, the philosopher releases both the fork and spoon.

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
    }
}

In this algorithm, each philosopher first thinks and then attempts to pick up the fork and spoon on their left. After eating, they put down both utensils. This approach ensures that no two philosophers can hold the same utensil at the same time, preventing deadlock.

Still, there is a possibility of deadlock. If all philosophers pick up the left cutlery at the same time, they will all be waiting for the cutlery at right, leading to deadlock. To prevent this, we can introduce a new rule, only N-1 philosophers can start eating at the same time, where N is the total number of philosophers. This prevents the circular wait condition, thus avoiding deadlock.

Implementation

Here is a simple implementation of the Dining Philosophers Problem using semaphores in C++/ Python and Java:

Note: We assumed both forks and spoons as same entity (stored in forks array) and ensured a philosopher must get left and right utensils to start eating. This will simplify the implementation.

import threading
import time
import random

N = 6
forks = [threading.Lock() for _ in range(N)]
waiter = threading.Semaphore(N - 1)
stop_event = threading.Event()     # <-- global stop flag

def philosopher(i):
    left = forks[i]
    right = forks[(i + 1) % N]
    while not stop_event.is_set():   # <-- stop condition
        print(f"Philosopher {i} is thinking.")
        time.sleep(random.uniform(0.2, 0.6))   # shorter for clarity

        if stop_event.is_set():
            break

        if not waiter.acquire(timeout=0.5):     # avoid blocking forever
            continue

        if not left.acquire(timeout=0.5):
            waiter.release()
            continue

        if not right.acquire(timeout=0.5):
            left.release()
            waiter.release()
            continue

        try:
            print(f"Philosopher {i} is eating.")
            time.sleep(random.uniform(0.2, 0.6))
        finally:
            right.release()
            left.release()
            waiter.release()

# Start threads
threads = []
for i in range(N):
    t = threading.Thread(target=philosopher, args=(i,))
    threads.append(t)
    t.start()

# Let the simulation run for 5 seconds
time.sleep(5)
stop_event.set()      # signal all philosophers to stop

# Wait for threads to finish
for t in threads:
    t.join()

print("Simulation finished.")

The output of the above code will be similar to below −

Philosopher 0 is thinking.
Philosopher 1 is thinking.
Philosopher 2 is thinking.
Philosopher 3 is thinking.
Philosopher 4 is thinking.
Philosopher 5 is thinking.
Philosopher 2 is eating.
Philosopher 5 is eating.
Philosopher 2 is thinking.
.......
Stimulation finished.
#include <iostream>
#include <thread>
#include <vector>
#include <chrono>
#include <mutex>
#include <condition_variable>
#include <atomic>
#include <random>

using namespace std::chrono_literals;

class Semaphore {
public:
    explicit Semaphore(int initial) : count(initial) {}
    // try to acquire within timeout; returns true if acquired
    bool try_acquire_for(std::chrono::milliseconds timeout) {
        std::unique_lock<std::mutex> lk(m);
        if (!cv.wait_for(lk, timeout, [&]{ return count > 0; })) return false;
        --count;
        return true;
    }
    void release() {
        std::lock_guard<std::mutex> lk(m);
        ++count;
        cv.notify_one();
    }
private:
    std::mutex m;
    std::condition_variable cv;
    int count;
};

int main() {
    const int N = 6;
    std::vector<std::timed_mutex> forks(N);
    Semaphore waiter(N - 1);
    std::atomic<bool> stop_flag{false};

    auto philosopher = [&](int i) {
        // per-thread RNG
        std::mt19937 rng(std::random_device{}() ^ (i << 8));
        std::uniform_int_distribution<int> think_ms(200, 600);
        std::uniform_int_distribution<int> eat_ms(200, 600);

        while (!stop_flag.load(std::memory_order_relaxed)) {
            std::cout << "Philosopher " << i << " is thinking." << std::endl;
            std::this_thread::sleep_for(std::chrono::milliseconds(think_ms(rng)));

            // request permission from the waiter (timeout so we can exit)
            if (!waiter.try_acquire_for(500ms)) continue;

            bool left_locked = false, right_locked = false;
            // try acquire left fork
            left_locked = forks[i].try_lock_for(500ms);
            if (!left_locked) {
                waiter.release();
                continue;
            }
            // try acquire right fork
            right_locked = forks[(i + 1) % N].try_lock_for(500ms);
            if (!right_locked) {
                forks[i].unlock();
                waiter.release();
                continue;
            }

            // Both forks acquired
            try {
                std::cout << "Philosopher " << i << " is eating." << std::endl;
                std::this_thread::sleep_for(std::chrono::milliseconds(eat_ms(rng)));
            } catch (...) {
                // not expected, but ensure release
            }

            // release forks and waiter
            forks[(i + 1) % N].unlock();
            forks[i].unlock();
            waiter.release();
        }
        // exiting
        //std::cout << "Philosopher " << i << " exiting.\n";
    };

    std::vector<std::thread> threads;
    threads.reserve(N);
    for (int i = 0; i < N; ++i) threads.emplace_back(philosopher, i);

    // run for 5 seconds
    std::this_thread::sleep_for(5s);
    stop_flag.store(true);

    for (auto &t : threads) if (t.joinable()) t.join();

    std::cout << "Simulation finished.\n";
    return 0;
}

The output of the above code will be similar to below −

Philosopher 0 is thinking.
Philosopher 1 is thinking.
Philosopher 2 is thinking.
Philosopher 3 is thinking.
Philosopher 4 is thinking.
Philosopher 5 is thinking.
Philosopher 3 is eating.
Philosopher 1 is eating.
Philosopher 3 is thinking.
.......
Simulation finished.
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock; 
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.Random;

public class Dining {
    static final int N = 6;

    public static void main(String[] args) throws InterruptedException {
        final ReentrantLock[] forks = new ReentrantLock[N];
        for (int i = 0; i < N; ++i) forks[i] = new ReentrantLock();

        final Semaphore waiter = new Semaphore(N - 1);
        final AtomicBoolean stop = new AtomicBoolean(false);

        Thread[] threads = new Thread[N];

        for (int i = 0; i < N; ++i) {
            final int id = i;

            threads[i] = new Thread(() -> {
                Random rng = new Random(System.nanoTime() ^ (id << 8));

                while (!stop.get()) {
                    System.out.println("Philosopher " + id + " is thinking.");
                    try {
                        Thread.sleep(200 + rng.nextInt(401)); // 200–600 ms
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        break;
                    }

                    boolean gotWaiter = false;
                    boolean leftLocked = false;
                    boolean rightLocked = false;

                    try {
                        gotWaiter = waiter.tryAcquire(500, TimeUnit.MILLISECONDS);
                        if (!gotWaiter) continue;

                        leftLocked = forks[id].tryLock(500, TimeUnit.MILLISECONDS);
                        if (!leftLocked) continue;

                        rightLocked = forks[(id + 1) % N].tryLock(500, TimeUnit.MILLISECONDS);
                        if (!rightLocked) continue;

                        System.out.println("Philosopher " + id + " is eating.");
                        Thread.sleep(200 + rng.nextInt(401));

                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        break;

                    } finally {
                        if (rightLocked) forks[(id + 1) % N].unlock();
                        if (leftLocked) forks[id].unlock();
                        if (gotWaiter) waiter.release();
                    }
                }
            });

            threads[i].start();
        }

        Thread.sleep(5000);
        stop.set(true);

        for (Thread t : threads) t.join();

        System.out.println("Simulation finished.");
    }
}

The output of the above code will be similar to below −

Philosopher 0 is thinking.
Philosopher 1 is thinking.
Philosopher 2 is thinking.
Philosopher 3 is thinking.
Philosopher 4 is thinking.
Philosopher 5 is thinking.
Philosopher 4 is eating.
Philosopher 2 is eating.
Philosopher 4 is thinking.
.......
Simulation finished.

Conclusion

The Dining Philosophers Problem is a classic example of how to manage resource sharing and synchronization in concurrent programming. The idea behind solving this problem using semaphores is to ensure that no two philosophers can hold the same utensil at the same time. We also introduced a waiter to limit the number of philosophers who can attempt to eat simultaneously. This prevents circular wait and thus avoids deadlock.

Advertisements