All chapters in this track
- 01Chapter 1: Concurrency vs. Parallelism, and Why It's Hard
- 02Chapter 2: Processes vs. Threads, Revisited for Concurrency
- 03Chapter 3: The Core Hazards: Race Conditions, Deadlock, Livelock, Starvation
- 04Chapter 4: Synchronization Primitives: Locks, Semaphores, Monitors, Condition Variables
- 05Chapter 5: Designing for Thread Safety: Immutability, Confinement, Thread-Safe Collections
- 06Chapter 6: Classic Patterns: Producer-Consumer, Reader-Writer, Thread Pools
- 07Chapter 7: Higher-Level Models: Async/Await, Event Loops, Coroutines, Actors, Channels
- 08Chapter 8: Memory Models and Visibility: Happens-Before, Volatile, Atomics
- 09Chapter 9: Lock-Free and Wait-Free Programming Basics
- 10Chapter 10: How Concurrency Differs Across Runtimes: JVM Threads, Python's GIL, Go's Goroutines, JavaScript's Event Loop
- 11Chapter 11: Debugging and Testing Concurrent Code
- 12Chapter 12: Classic Interview Problem: Producer-Consumer
- 13Chapter 13: Classic Interview Problem: Dining Philosophers
- 14Chapter 14: Classic Interview Problem: Readers-Writers
- 15Chapter 15: Concurrency in System Design and Production Debugging
Chapter 13 of 15
Chapter 13: Classic Interview Problem: Dining Philosophers
Why this matters
The dining philosophers problem is the canonical teaching example for deadlock specifically, precisely because its setup makes the circular-wait condition (companion Operating System book, Chapter 10) unavoidable to confront directly — there's no way to solve it correctly without explicitly reasoning about all four deadlock conditions from that chapter.
The problem, precisely stated
Five philosophers sit around a circular table, alternating between thinking and eating. Between each pair of adjacent philosophers is exactly one fork — so there are five forks total, but each philosopher needs both their left and right fork to eat. Design an algorithm so that all philosophers can eat over time, without deadlock or starvation.
Philosopher 0
/ \
Fork 4 Fork 0
| |
Philosopher 4 Philosopher 1
| |
Fork 3 Fork 1
\ /
Philosopher 3 - Fork 2 - Philosopher 2
The naive solution, and exactly why it deadlocks
import threading
NUM_PHILOSOPHERS = 5
forks = [threading.Lock() for _ in range(NUM_PHILOSOPHERS)]
def philosopher(i):
left_fork = forks[i]
right_fork = forks[(i + 1) % NUM_PHILOSOPHERS]
for _ in range(3):
# think()
with left_fork: # pick up left fork
with right_fork: # pick up right fork
pass # eat()
# DEADLOCK RISK: if EVERY philosopher picks up their left fork at the
# same moment, EVERY philosopher is now holding one fork and waiting
# for their right fork -- which is some OTHER philosopher's left fork,
# already held. A perfect circular wait, all five conditions from the
# OS book's Chapter 10 satisfied simultaneously.
A strong candidate names the exact failure precisely: "If all five philosophers pick up their left fork simultaneously, each is now waiting for their right fork, which is the next philosopher's left fork — already held. This is a textbook circular wait: mutual exclusion (a fork can only be held by one philosopher), hold-and-wait (each holds one fork while waiting for another), no preemption (a fork can't be forcibly taken), and circular wait (the ring structure itself)."
Solution 1: resource (lock) ordering — break the circular wait
[SENIOR INSIGHT] Directly applying the companion OS book's Chapter 10 standard fix: if every philosopher picks up forks in a consistent, global order (e.g., always the lower-numbered fork first, regardless of which is "left" or "right" for them), the circular wait becomes structurally impossible — there must always be at least one philosopher for whom "lower-numbered first" happens to also be picking up second the fork that would otherwise complete a cycle.
def philosopher_ordered(i):
left_fork = forks[i]
right_fork = forks[(i + 1) % NUM_PHILOSOPHERS]
first_fork, second_fork = (left_fork, right_fork) if i < (i + 1) % NUM_PHILOSOPHERS else (right_fork, left_fork)
# Every philosopher picks up the LOWER-NUMBERED fork first, breaking the symmetry
# that made a synchronized circular wait possible in the naive version.
for _ in range(3):
with first_fork:
with second_fork:
pass # eat()
Solution 2: a resource hierarchy via an arbitrator (limit concurrent diners)
[BEGINNER] An alternative fix: introduce a semaphore (Chapter 4) allowing at most N-1 philosophers to attempt picking up forks simultaneously (4 out of 5, here) — guaranteeing at least one philosopher is always waiting entirely before touching any fork, which makes a full circular wait among all five mathematically impossible, since one philosopher never enters the fork-acquisition phase at all while the others are mid-acquisition.
dining_semaphore = threading.Semaphore(NUM_PHILOSOPHERS - 1) # at most 4 can attempt at once
def philosopher_semaphore(i):
left_fork = forks[i]
right_fork = forks[(i + 1) % NUM_PHILOSOPHERS]
for _ in range(3):
with dining_semaphore: # only 4 of the 5 philosophers can be in this block at once
with left_fork:
with right_fork:
pass # eat()
Comparing the two solutions
| Resource ordering | Arbitrator (semaphore) | |
|---|---|---|
| Mechanism | Breaks circular wait directly, structurally | Prevents all 5 from competing simultaneously |
| Concurrency | All 5 can attempt to eat concurrently (just in a fixed pickup order) | At most 4 can attempt at once — slightly more conservative |
| Generalizes to N resources easily? | Yes — the "always acquire lower-numbered first" rule scales directly | Yes — the "N-1 concurrent attempts" rule also scales directly |
Both are valid, complete, standard solutions — a strong candidate can present either, but should be able to explain why it prevents the specific circular-wait condition, not just present code that happens to work.
Common mistakes
- Presenting a "solution" that reduces the deadlock's probability without actually eliminating it (e.g., "just add a random small delay before picking up the second fork") — this is not a real fix; it changes timing but doesn't structurally prevent the circular wait, and the deadlock remains possible, just rarer.
- Not being able to name which of the four deadlock conditions the fix actually breaks — a strong answer explicitly connects the fix to the specific condition it eliminates (resource ordering breaks circular wait; the semaphore approach also effectively breaks circular wait by guaranteeing at least one philosopher can't be mid-hold-and-wait).
- Forgetting to also verify no starvation is introduced by the fix. The ordering solution, for instance, should be checked for whether it could unfairly favor certain philosophers over others under sustained contention — a complete answer at least mentions this as a follow-up consideration, even if the basic fix doesn't fully resolve it.
- Not recognizing the real-world generalization: any system acquiring multiple locks/resources in different orders across different code paths is exposed to exactly this same risk, at any scale — dining philosophers is a small, concrete stand-in for a very common, very real production hazard.
Discussion / interview questions
-
"Walk through why the naive dining philosophers solution can deadlock, naming the specific conditions involved." Strong answer: If every philosopher picks up their left fork simultaneously, all five hold one fork while waiting for another already-held fork — satisfying mutual exclusion, hold-and-wait, no preemption, and circular wait simultaneously, the exact four conditions from the OS book's Chapter 10.
-
"How does resource ordering fix the dining philosophers deadlock?" Strong answer: Requiring every philosopher to pick up the lower-numbered fork first (rather than their "left" fork specifically) breaks the symmetry that allowed a full circular wait to form — there's always at least one philosopher for whom this rule prevents completing the cycle.
-
"What's an alternative fix to resource ordering, and how does it work?" Strong answer: An arbitrator (e.g., a semaphore limiting concurrent fork-acquisition attempts to N-1 philosophers) guarantees at least one philosopher is never mid-acquisition at the same time as the others, making a full circular wait among all N impossible.
-
"Is adding a small random delay before picking up the second fork a valid fix for this problem?" Strong answer: No — it reduces the probability of the deadlock occurring but doesn't structurally eliminate the circular wait condition; the deadlock remains genuinely possible, just statistically rarer, which is not the same as a correct fix.
Chapter summary / cheat sheet
- Dining philosophers is the canonical deadlock teaching problem — its naive solution deadlocks via a perfect circular wait if every philosopher picks up their "first" fork simultaneously.
- Resource ordering (always acquire the lower-numbered resource first) structurally breaks the circular wait — the standard, general-purpose fix, directly from the OS book's Chapter 10.
- Arbitrator/semaphore (limit concurrent attempts to N-1) is an equally valid alternative, preventing the specific condition needed for all N to be simultaneously mid-acquisition.
- A statistical mitigation (random delays) is not a real fix — always require and explain a structural elimination of at least one of the four necessary deadlock conditions.
- This problem is a small stand-in for a very real, very common production hazard: any system acquiring multiple locks in inconsistent orders across different code paths.