Dining Philosophers and Deadlock Analysis
What This Concept Is
Dijkstra's dining philosophers is a minimal setting where deadlock, livelock, and starvation can all appear.
Five philosophers sit around a circular table. Between each pair is a single fork. To eat, a philosopher needs both adjacent forks. The naive protocol is:
// philosopher i
pick_up(fork[i]);
pick_up(fork[(i + 1) % 5]);
eat();
put_down(fork[(i + 1) % 5]);
put_down(fork[i]);
The failure mode: all five philosophers simultaneously pick up their left fork, then all wait forever for their right fork. Nobody holds both; nobody can proceed. Deadlock.
The standard fixes correspond one-to-one to the four Coffman conditions for deadlock (formalized in concept 13):
- Break mutual exclusion -- not possible here; the fork is a shared resource that cannot be duplicated.
- Break hold and wait -- pick up both forks atomically under a table mutex, or release the first if the second is unavailable (adds livelock risk).
- Break no preemption -- allow a philosopher to be forced to release a held fork. Rare in user code; common in databases (lock timeouts, abort-and-retry).
- Break circular wait -- impose a total order on forks; always pick up the lower-numbered fork first.
Why It Matters Here
Dining philosophers is not just a cute problem. Every database transaction, every file-system lock acquisition, every compound lock operation in a real system is a philosopher at a table. The lessons transfer directly:
- establish a total lock order; document it
- prefer all-or-nothing lock acquisition (
std::scoped_lock,pthread_mutex_trylock+ rollback) - detect cycles if prevention is impossible
This concept also sets up the formal treatment of deadlock in concept 13.
Concrete Example
Resource ordering fix (breaks circular wait):
void philosopher(int i) {
int low = (i < (i + 1) % 5) ? i : (i + 1) % 5;
int high = low ^ i ^ ((i + 1) % 5); // the other one
pthread_mutex_lock(&fork[low]);
pthread_mutex_lock(&fork[high]);
eat();
pthread_mutex_unlock(&fork[high]);
pthread_mutex_unlock(&fork[low]);
}
Now philosopher 4 (who sits between fork 4 and fork 0) picks up fork 0 first, breaking the cycle. At most four philosophers can be holding their "first" fork simultaneously, so at least one holds the higher-numbered fork of a pair and can always finish.
Arbitrator fix (breaks hold-and-wait): a table mutex is acquired before any philosopher can attempt to take forks; the philosopher atomically tests both forks and either takes both or releases the mutex and waits. This serializes the approach but not the eating.
Trylock + backoff (attempts to break hold-and-wait, risks livelock):
while (true) {
pthread_mutex_lock(&fork[i]);
if (pthread_mutex_trylock(&fork[(i + 1) % 5]) == 0) break;
pthread_mutex_unlock(&fork[i]);
nanosleep(&backoff, NULL);
}
If all philosophers retry in lockstep, they will all back off, all retry, all back off -- livelock. Randomized backoff helps but does not prove progress.
Common Confusion / Misconception
"Using try_lock avoids deadlock." It avoids this specific deadlock, but it introduces livelock risk and does not compose. Two code paths each using try_lock may together still violate progress.
"If I always lock mutexes in the order they are declared, I cannot deadlock." Only if every code path in your program does this, and only if the "declaration order" is globally defined. In practice, lock order must be an invariant you enforce, not a side effect.
"Deadlock is rare in practice." In single-process programs with two or three mutexes, maybe. In databases, distributed systems, and kernels, deadlock is a routine operational concern with dedicated detection and recovery mechanisms.
How To Use It
When a code path needs to acquire more than one lock:
- Decide, for every pair of locks that can be held simultaneously anywhere in the codebase, a strict order.
- Enforce the order at every acquisition point. Use
std::scoped_lock/std::lockin C++ to acquire multiple locks atomically with built-in ordering. - If order cannot be established (for example, because the locks depend on runtime identity), use trylock + rollback + randomized backoff and deadlock detection.
- Hold locks for as short a critical section as is correct; the longer you hold, the more cycles are possible.
Check Yourself
- Why does "all five philosophers pick up left fork, then right fork" deadlock?
- Which Coffman condition does resource ordering break?
- Why is trylock + backoff not a general-purpose solution?
Mini Drill or Application
Implement dining philosophers in C + pthreads with the deadlock-prone version. Run with 5 threads; observe that within seconds, all philosophers are blocked. Now apply the resource-ordering fix. Verify progress. Annotate the code showing which Coffman condition the fix broke.