Loading content...
Semaphores, invented by Dijkstra himself, are the natural first tool to apply to a problem Dijkstra conceived. The P (wait) and V (signal) operations provide exactly the kind of atomic, blocking synchronization we need to coordinate chopstick access.
In this page, we will develop and analyze multiple semaphore-based solutions, progressing from simple approaches to sophisticated ones that provide strong guarantees. Each solution illuminates different aspects of concurrent system design.
After completing this page, you will be able to implement multiple semaphore solutions to the Dining Philosophers Problem, understand the guarantees and limitations of each approach, analyze correctness properties, and select the appropriate solution for different requirements.
Before applying semaphores to our problem, let's ensure complete clarity on their semantics. A semaphore is an integer variable S with two atomic operations:
P (Proberen / Wait / Down):
wait(S):
while (S <= 0) block;
S = S - 1;
V (Verhogen / Signal / Up):
signal(S):
S = S + 1;
wake one blocked process (if any)
The critical property is atomicity: the test-and-decrement (P) and increment (V) operations are indivisible. No race conditions occur within the semaphore operations themselves.
| Semaphore Type | Initial Value | Range | Use in Our Problem |
|---|---|---|---|
| Binary Semaphore | 0 or 1 | {0, 1} | Model each chopstick (available/held) |
| Counting Semaphore | N ≥ 0 | [0, N] | Limit concurrent eating philosophers |
| Mutex | 1 | {0, 1} | Protect critical sections (state changes) |
Key Properties We Rely On:
Whether a semaphore uses FIFO wakeup or arbitrary wakeup affects starvation guarantees. Most modern implementations use FIFO, but always verify for your platform. Our analysis will note when FIFO is required.
Our first correct solution employs a simple but powerful insight: deadlock in the naive solution occurs when all N philosophers hold their left chopstick. If we limit the system to at most N-1 philosophers attempting to eat simultaneously, at least one will always be able to acquire both chopsticks.
The Insight:
With 5 philosophers and 5 chopsticks:
Implementation:
123456789101112131415161718192021222324252627282930
#define N 5 semaphore chopstick[N]; // Each initialized to 1 (binary semaphore)semaphore room; // Counting semaphore, initialized to N-1 = 4 void philosopher(int i) { while (true) { think(); // Only N-1 philosophers can enter the "room" (attempt to eat) wait(room); // Enter room (blocks if 4 already inside) wait(chopstick[i]); // Pick up left chopstick wait(chopstick[(i + 1) % N]); // Pick up right chopstick eat(); signal(chopstick[(i + 1) % N]); // Put down right chopstick signal(chopstick[i]); // Put down left chopstick signal(room); // Leave room (allow another to enter) }} void initialize() { for (int i = 0; i < N; i++) { chopstick[i] = 1; } room = N - 1; // At most 4 philosophers can attempt eating}Correctness Analysis:
Deadlock Freedom: With at most N-1 philosophers attempting to eat, and N chopsticks, at least one philosopher can always acquire both chopsticks. The circular wait condition cannot hold.
Proof: Assume N-1 philosophers are in the room. They collectively need 2(N-1) = 2N-2 chopstick acquisitions. With N chopsticks, each used by at most 1 philosopher at a time, and ⌊N/2⌋ philosophers can eat simultaneously, deadlock is impossible.
Mutual Exclusion: Each chopstick semaphore ensures only one philosopher holds it.
Starvation Freedom: If the room semaphore uses FIFO queuing, philosophers waiting to enter will eventually get their turn. Combined with FIFO chopstick semaphores, starvation is prevented.
| Property | Guaranteed? | Explanation |
|---|---|---|
| Deadlock Freedom | ✅ Yes | At most N-1 contenders for N resources |
| Mutual Exclusion | ✅ Yes | Binary semaphores on chopsticks |
| Starvation Freedom | ✅ With FIFO semaphores | FIFO room + chopstick queues |
| Maximum Concurrency | ❌ Reduced | One philosopher always excluded |
This solution sacrifices potential parallelism (one slot always vacant) for simplicity and strong guarantees. For 5 philosophers, we can still have 2 eating simultaneously—the room limit doesn't further restrict concurrent eating, just attempts.
Another elegant approach breaks the symmetry of the problem. The deadlock scenario arises because all philosophers follow the same protocol (left first, then right). If we have some philosophers follow a different order, the cycle cannot form.
The Strategy:
This asymmetry breaks the circular wait chain.
12345678910111213141516171819202122232425
#define N 5 semaphore chopstick[N]; // Each initialized to 1 void philosopher(int i) { while (true) { think(); if (i % 2 == 0) { // Even philosophers: left first, then right wait(chopstick[i]); wait(chopstick[(i + 1) % N]); } else { // Odd philosophers: right first, then left wait(chopstick[(i + 1) % N]); wait(chopstick[i]); } eat(); // Release in any order (doesn't affect correctness) signal(chopstick[i]); signal(chopstick[(i + 1) % N]); }}Why This Prevents Deadlock:
Consider the potential deadlock scenario. For a cycle to form, we need:
P0 → waiting for P1's resource → P1 → waiting for P2's resource → ... → P4 → waiting for P0's resource
With asymmetric acquisition:
The asymmetry means adjacent philosophers never follow the same acquisition pattern, preventing the lockstep that creates deadlock.
Detailed Proof:
Assume deadlock exists. Then every philosopher holds one chopstick and waits for another. Consider the chopsticks as a directed graph where edges represent wait relationships.
For P0 (even): holds C0, waits for C1 → edge C0 → C1 For P1 (odd): holds C2, waits for C1 → edge C2 → C1 For P2 (even): holds C2, waits for C3 → edge C2 → C3 ...
But P1 and P2 both involve C2, and P2 should hold C2 (as an even philosopher picking left first). Conflict! The assumed deadlock configuration is impossible.
More formally: the even/odd split ensures no global cycle in the wait graph because the acquisition order alternates around the circle.
| Property | Guaranteed? | Explanation |
|---|---|---|
| Deadlock Freedom | ✅ Yes | Asymmetric acquisition breaks cycles |
| Mutual Exclusion | ✅ Yes | Binary semaphores on chopsticks |
| Starvation Freedom | ✅ With FIFO semaphores | No structural starvation; FIFO ensures fairness |
| Maximum Concurrency | ✅ Yes | All ⌊N/2⌋ can eat simultaneously |
This solution achieves both deadlock freedom and maximum concurrency without additional semaphores. The only overhead is the conditional in each philosopher's code—negligible in practice.
Andrew Tanenbaum's classic operating systems textbook presents an elegant solution that models philosopher states explicitly. Each philosopher is in one of three states: THINKING, HUNGRY, or EATING. Philosophers only pick up chopsticks when both are available, avoiding the hold-and-wait condition.
The State Machine:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
#define N 5#define LEFT(i) ((i + N - 1) % N)#define RIGHT(i) ((i + 1) % N) typedef enum { THINKING, HUNGRY, EATING } State; State state[N]; // State of each philosophersemaphore mutex = 1; // Protects state arraysemaphore s[N]; // One semaphore per philosopher, all initialized to 0 void test(int i) { // Check if philosopher i can start eating // Called while holding mutex if (state[i] == HUNGRY && state[LEFT(i)] != EATING && state[RIGHT(i)] != EATING) { state[i] = EATING; signal(s[i]); // Wake up philosopher i if blocked }} void take_forks(int i) { wait(mutex); state[i] = HUNGRY; test(i); // Try to acquire both forks signal(mutex); wait(s[i]); // Block if forks not acquired} void put_forks(int i) { wait(mutex); state[i] = THINKING; test(LEFT(i)); // Let left neighbor try test(RIGHT(i)); // Let right neighbor try signal(mutex);} void philosopher(int i) { while (true) { think(); take_forks(i); // Acquire both forks atomically eat(); put_forks(i); // Release and notify neighbors }}How It Works:
take_forks(i): Philosopher i becomes HUNGRY and calls test(i). If both neighbors are not EATING, philosopher i becomes EATING and signals their semaphore. Otherwise, they remain HUNGRY and block on wait(s[i]).
put_forks(i): Philosopher i finishes eating, becomes THINKING, and calls test() on both neighbors. This may wake up a hungry neighbor who can now eat.
test(i): The critical function that atomically checks if philosopher i can eat (HUNGRY + neither neighbor EATING). If so, transitions to EATING and signals.
Why No Deadlock:
The key insight is that philosophers never hold one chopstick while waiting for another. The state check is atomic (protected by mutex). A philosopher either:
There is no hold-and-wait, so circular wait is impossible.
Correctness Proof Sketch:
Mutual Exclusion: If philosopher i is EATING, then state[i] == EATING. The test() function only sets EATING when state[LEFT(i)] != EATING and state[RIGHT(i)] != EATING. Hence, no two adjacent philosophers can be EATING.
Deadlock Freedom: Deadlock would require all philosophers to be HUNGRY and blocked. But if philosopher i is HUNGRY and both neighbors are not EATING, test(i) would succeed. If test(i) failed, at least one neighbor is EATING. That neighbor will eventually call put_forks(), which calls test(i) again. Progress is guaranteed.
Starvation (Potential Issue): A philosopher flanked by two frequently-eating neighbors could be repeatedly skipped. The basic Tanenbaum solution does not guarantee starvation freedom without additional mechanisms.
| Property | Guaranteed? | Explanation |
|---|---|---|
| Deadlock Freedom | ✅ Yes | No hold-and-wait; atomic acquisition |
| Mutual Exclusion | ✅ Yes | State check prevents adjacent EATING |
| Starvation Freedom | ❌ Not guaranteed | May be bypassed by neighbors |
| Maximum Concurrency | ✅ Yes | ⌊N/2⌋ can eat simultaneously |
While deadlock-free, this solution can starve a philosopher if their neighbors alternate eating. Solutions like adding priority aging or timestamps to the HUNGRY state can provide starvation freedom.
To eliminate starvation, we can enhance the Tanenbaum solution with priority based on waiting time. A philosopher who has been waiting longer gets priority over neighbors.
The Enhancement:
We add a timestamp recording when each philosopher became HUNGRY. In the test() function, a philosopher only eats if neighbors aren't both EATING and aren't HUNGRY with earlier timestamps.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
#define N 5#define LEFT(i) ((i + N - 1) % N)#define RIGHT(i) ((i + 1) % N) typedef enum { THINKING, HUNGRY, EATING } State; State state[N];semaphore mutex = 1;semaphore s[N]; // All initialized to 0unsigned long hungry_since[N]; // Timestamp when became HUNGRYunsigned long clock = 0; bool has_priority(int i, int neighbor) { // i has priority over neighbor if: // - neighbor is not HUNGRY, OR // - i has been waiting longer if (state[neighbor] != HUNGRY) return true; return hungry_since[i] < hungry_since[neighbor];} void test(int i) { // Philosopher i can eat if: // 1. They are HUNGRY // 2. Neither neighbor is EATING // 3. They have priority over hungry neighbors if (state[i] == HUNGRY && state[LEFT(i)] != EATING && state[RIGHT(i)] != EATING && has_priority(i, LEFT(i)) && has_priority(i, RIGHT(i))) { state[i] = EATING; signal(s[i]); }} void take_forks(int i) { wait(mutex); state[i] = HUNGRY; hungry_since[i] = ++clock; // Record when became hungry test(i); signal(mutex); wait(s[i]);} void put_forks(int i) { wait(mutex); state[i] = THINKING; test(LEFT(i)); test(RIGHT(i)); signal(mutex);}Starvation Freedom Proof:
Suppose philosopher i has been HUNGRY since time T. As time progresses:
This is bounded waiting: no philosopher waits indefinitely while others eat repeatedly. The bound is O(N) eating cycles.
| Property | Guaranteed? | Explanation |
|---|---|---|
| Deadlock Freedom | ✅ Yes | Same as base Tanenbaum |
| Mutual Exclusion | ✅ Yes | Same as base Tanenbaum |
| Starvation Freedom | ✅ Yes | Priority by waiting time |
| Maximum Concurrency | ✅ Yes | Same as base Tanenbaum |
| Bounded Waiting | ✅ Yes | O(N) eating cycles maximum wait |
This fair solution provides all desirable properties: deadlock freedom, mutual exclusion, starvation freedom, and maximum concurrency. The only cost is the additional timestamp bookkeeping—negligible overhead in practice.
Let us compare all the semaphore-based solutions we have studied, providing guidance for selecting the appropriate one:
Summary Comparison:
| Solution | Deadlock Free | Starvation Free | Max Concurrency | Complexity | Semaphores Used |
|---|---|---|---|---|---|
| Naive (left-first) | ❌ | ❌ | ✅ | Trivial | N (chopsticks) |
| Limit to N-1 | ✅ | ✅ (FIFO) | ⚠️ Reduced | Simple | N + 1 (room) |
| Asymmetric (odd/even) | ✅ | ✅ (FIFO) | ✅ | Simple | N (chopsticks) |
| Tanenbaum State | ✅ | ❌ | ✅ | Medium | N + 1 (s[i] + mutex) |
| Fair Tanenbaum | ✅ | ✅ | ✅ | Medium+ | N + 1 + timestamps |
Decision Guide:
Performance Considerations:
In practice, the performance difference between these solutions is minimal for small N. The overhead of semaphore operations (typically microseconds) dominates any difference in algorithm complexity.
For large-scale systems with many "philosophers" (e.g., database connections, worker threads), other considerations become important:
When implementing semaphore solutions in production systems, several practical considerations arise:
POSIX Semaphores:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
#include <semaphore.h>#include <pthread.h> #define N 5 sem_t chopstick[N]; void* philosopher(void* arg) { int i = *(int*)arg; while (1) { // Think usleep(rand() % 1000); // Asymmetric acquisition if (i % 2 == 0) { sem_wait(&chopstick[i]); sem_wait(&chopstick[(i + 1) % N]); } else { sem_wait(&chopstick[(i + 1) % N]); sem_wait(&chopstick[i]); } // Eat printf("Philosopher %d eating", i); usleep(rand() % 1000); // Release sem_post(&chopstick[i]); sem_post(&chopstick[(i + 1) % N]); } return NULL;} int main() { pthread_t threads[N]; int ids[N]; // Initialize semaphores for (int i = 0; i < N; i++) { sem_init(&chopstick[i], 0, 1); // Process-private, initial value 1 ids[i] = i; } // Create philosopher threads for (int i = 0; i < N; i++) { pthread_create(&threads[i], NULL, philosopher, &ids[i]); } // Wait (in practice, would have termination condition) for (int i = 0; i < N; i++) { pthread_join(threads[i], NULL); } // Cleanup for (int i = 0; i < N; i++) { sem_destroy(&chopstick[i]); } return 0;}Common Pitfalls:
sem_init() before first use.Add logging with philosopher ID and operation for debugging. Use tools like ThreadSanitizer, Helgrind, or Valgrind to detect races. Test with varying sleep times and high iteration counts to expose timing-dependent bugs.
We have developed and analyzed multiple semaphore-based solutions to the Dining Philosophers Problem, each with distinct characteristics:
What's Next:
The semaphore solutions break deadlock through various clever mechanisms. The next page explores a more systematic approach: Resource Ordering. By imposing a global order on chopsticks and requiring acquisition in order, we structurally prevent circular wait—a technique that generalizes to arbitrary resource allocation systems.
You now possess a comprehensive understanding of semaphore-based solutions to the Dining Philosophers Problem. You can implement deadlock-free and starvation-free solutions using counting semaphores, binary semaphores, and state-based protocols. This knowledge directly applies to any resource allocation problem in concurrent systems.