Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 25

Dining Philosophers: A Deadlock Simulation

Recommended resources

Hello! Welcome to our next lesson on deadlock.

In the past few lessons, we've explored the theory behind deadlock, identifying its four necessary conditions. We also covered two powerful prevention strategies: enforcing a strict lock-ordering policy and using the more dynamic "try-and-backoff" pattern with pthread_mutex_trylock.

Today, we'll bring these concepts to life by studying the Dining Philosophers problem. This is a classic, canonical problem in computer science that serves as an excellent case study for deadlock. Our goal is to simulate this problem in C to demonstrate exactly how a deadlock scenario can arise from a seemingly logical design.

This lesson is all about understanding and reproducing the problem. In the next lesson, we'll focus on solving it.

The Dining Philosophers Problem

The Dining Philosophers problem is a metaphor for the challenges of resource allocation in multi-threaded or multi-process systems. Let's start with a video that clearly explains the scenario.

Deadlock 3: Dining Philosophers

The video 'Deadlock 3: Dining Philosophers' from the Jacob Schrum channel provides a concise introduction to the problem's setup and rules.

Please watch the first 2 minutes of the video (from 00:00 to 02:05). This will introduce the philosophers, the forks, and the basic rules that set the stage for deadlock.

To summarize the setup:

  • There are five philosophers sitting at a round table.
  • Between each pair of adjacent philosophers is a single fork (or chopstick). So, five philosophers, five forks.
  • A philosopher's life consists of two alternating states: thinking and eating.
  • To eat, a philosopher must pick up both the fork to their left and the fork to their right.
  • A fork is a resource that can only be used by one philosopher at a time (mutual exclusion).

This seemingly simple scenario contains all the ingredients for deadlock.


From Philosophy to Pthreads: Mapping to Code

Let's translate this abstract problem into the concrete terms of C and pthreads:

  • Philosophers are threads.
  • Forks are shared resources, which we will protect with mutexes.
  • Thinking can be simulated with a sleep() or another non-critical computation.
  • Eating is the critical section where a thread must hold two mutex locks simultaneously.

The naive and most intuitive approach is for each philosopher to do the same thing:

  1. Think for a while.
  2. Pick up the fork on their left.
  3. Pick up the fork on their right.
  4. Eat for a while.
  5. Put down the right fork.
  6. Put down the left fork.
  7. Repeat.

Why does this lead to deadlock? Let's revisit the four conditions in this context.

CS170 Lecture notes -- Dining Philosophers

The lecture notes 'CS170 Lecture notes -- Dining Philosophers' from UC Santa Barbara clearly list the four necessary conditions for deadlock. Let's quickly review them.

Read the four bullet points under the heading 'Dealing with Deadlock' that define Mutual Exclusion, Hold-and-Wait, Circular Wait, and No Preemption.

The Dining Philosophers problem with the naive algorithm satisfies all four conditions:

  1. Mutual Exclusion: A fork can only be used by one philosopher at a time. A pthread_mutex_t inherently enforces this.
  2. Hold and Wait: A philosopher successfully locks their left fork's mutex and holds it while waiting to acquire the lock for their right fork.
  3. No Preemption: We cannot forcibly take a fork (release a mutex) from another philosopher (thread).
  4. Circular Wait: This is the fatal flaw. If every philosopher picks up their left fork at roughly the same time, then Philosopher 0 is waiting for Philosopher 1's fork, Philosopher 1 is waiting for Philosopher 2's, and so on, until Philosopher 4 is waiting for Philosopher 0's fork.

This circular dependency is the deadlock.

Dining Philosophers Deadlock Illustration
This image illustrates the deadlock state. Each philosopher has acquired one fork and is waiting for the fork held by their neighbor, creating a circular wait where no one can proceed.

Let's see this circular wait explained visually.

Deadlock 3: Dining Philosophers

Let's return to the 'Deadlock 3: Dining Philosophers' video, which now demonstrates how the symmetric logic leads to a circular wait.

Please watch from 02:05 to 07:33. The video first presents pseudo-code for the symmetric 'pick up right, then left' logic, then uses a resource allocation graph to show exactly how the circular wait forms, resulting in deadlock.


Implementing the Deadlock

Now, let's move from theory to practice. Your task is to write a C program that simulates this problem and reliably triggers the deadlock. The logic is surprisingly simple.

The following resource provides a complete, minimal C implementation that demonstrates the deadlock.

CS3130: Dining Philosophers

The webpage 'CS3130: Dining Philosophers' from the University of Virginia provides a clean, concise C code example that implements the deadlocking scenario using pthreads and mutexes.

Focus on the code block under the 'Deadlocking code' section. Copy this code into a file named dp_deadlock.c. Also, note the compilation instructions provided: clang -O2 -lpthread dp.c && ./a.out (or use gcc).

Let's analyze the core of the eat function from that resource:

void *eat(void *arg) {
    int n = (int) (long)arg; // Philosopher number (0 to 4)

    // A philosopher tries to take the chopstick with the same index first (e.g., their 'left'),
    // then the next one in the circle.
    pthread_mutex_lock(&chopstick[n]);
    printf("Philosopher %d got chopstick %d\n", n, n);
    
    // The moment of truth: try to get the second chopstick.
    pthread_mutex_lock(&chopstick[(n+1)%5]);
    printf("Philosopher %d got chopstick %d\n", n, (n+1)%5);

    printf ("Philosopher %d is eating\n",n);
    sleep(1);
    
    // Release the chopsticks (in reverse order).
    printf("Philosopher %d set down chopstick %d\n", n, (n+1)%5);
    pthread_mutex_unlock(&chopstick[(n+1)%5]);
    printf("Philosopher %d set down chopstick %d\n", n, n);
    pthread_mutex_unlock(&chopstick[n]);

    return NULL;
}

The logic is identical for all five threads, creating the perfect symmetry that leads to deadlock.

Test your understanding!

Part 1: Compile and Run
Compile the code from the CS3130 resource and run it. Because thread scheduling is non-deterministic, you may need to run it several times.

  • What do you observe? Does it deadlock every time? You might see it complete successfully, or you might see it hang after all five philosophers have acquired one chopstick.

Part 2: Forcing the Deadlock
As noted in the resources, whether a deadlock occurs can depend on system load and scheduling luck. We can make the deadlock condition much more likely by introducing a small delay between acquiring the first and second locks.

Modify your eat function to add a one-second pause after a philosopher picks up their first chopstick.

    pthread_mutex_lock(&chopstick[n]);
    printf("Philosopher %d got chopstick %d\n", n, n);
    
    sleep(1); // Give other threads a chance to grab their first chopstick.

    pthread_mutex_lock(&chopstick[(n+1)%5]);
    // ...
  • Now compile and run the modified code. What happens?
Show answer

After adding the sleep(1) call, the program should reliably deadlock every time. The output will look something like this, and then hang indefinitely:

Philosopher 1 got chopstick 1
Philosopher 0 got chopstick 0
Philosopher 3 got chopstick 3
Philosopher 2 got chopstick 2
Philosopher 4 got chopstick 4

The sleep(1) call creates a window of opportunity. After one philosopher grabs their first fork and goes to sleep, the scheduler gives other threads time to run. This allows all the other philosophers to also grab their first fork. When they all wake up and try to grab their second fork, they find it's already locked by their neighbor. Every thread is now blocked in pthread_mutex_lock, waiting for a resource that will never be released. This is a classic deadlock.

This simple exercise perfectly demonstrates how a symmetric resource acquisition strategy among multiple threads can lead to a complete system standstill.


Conclusion

In this lesson, we demystified the Dining Philosophers problem and brought it to life with a practical C implementation.

Key Takeaways:

  • The Dining Philosophers problem is a classic metaphor for resource contention and deadlock in concurrent systems.
  • We can model the problem using threads (philosophers) and mutexes (forks).
  • A naive, symmetric algorithm where every thread attempts to acquire resources in the same relative order (e.g., "left then right") creates a circular wait condition, leading to deadlock.
  • Due to the non-deterministic nature of thread scheduling, a deadlock-prone program might not fail on every execution. Techniques like adding a sleep() can help expose the underlying flaw by creating a window for the race condition to occur reliably.

Preview of the Next Lesson:
We've successfully created a deadlock. Now, how do we fix it? In our next lesson, we will solve the dining philosophers problem without deadlock by applying a lock-ordering hierarchy. This will involve breaking the symmetry that caused the circular wait, directly applying the deadlock prevention strategy we studied earlier.