The dining philosophers problem is a classical synchronization problem. Taken at face value, it is a pretty meaningless problem, but it is typical of many synchronization problems that you will see when allocating resources in operating systems, and in parallel/distributed systems.
Silberschatz & Galvin has an excellent description of dining philosophers. I'll be a little more sketchy.
The problem is defined as follows: There are 5 philosophers sitting at a round table. Between each adjacent pair of philosophers is a chopstick. In other words, there are five chopsticks. Each philosopher does two things: think and eat. The philosopher thinks for a while, and then stops thinking and becomes hungry. When the philosopher becomes hungry, he/she cannot eat until he/she owns the chopsticks to his/her left and right. When the philosopher is done eating he/she puts down the chopsticks and begins thinking again.
Of course, the definition of this problem always leads me to ask a few questions:
The challenge in the dining philosophers problem is to design a protocol so that the philosophers do not deadlock (i.e. every philosopher has a chopstick), and so that no philosopher starves (i.e. when a philosopher is hungry, he/she eventually gets the chopsticks). Additionally, our protocol should try to be as efficient as possible -- in other words, we should try to minimize the time that philosophers spend being hungry.
(In case you're bored, here is that last paragraph in the inimitable words of professor Wolski, back when he taught at UT: ``Since these are either unwashed, stubborn and deeply committed philosophers or unwashed, clueless, and basically helpless philosophers, there is a possibility for deadlock. In particular, if all philosophers simultaneously grab the chopstick on their left and then reach for the chopstick on their right (waiting until one is available) before eating, they will all starve. The challenge in the dining philosophers problem is to design a protocol so that the philosophers do not deadlock (i.e. the entire set of philosophers does not stop and wait indefinitely), and so that no philosopher starves (i.e. every philosopher eventually gets his/her hands on a pair of chopsticks).'')
A solution's data is going to be stored by src/dphil_driver.c as a (void *), which is returned here. The two procedures below pass the (void *) back to the solution. This is how we allow flexibility in the solution.
dphil num-philosophers max-think max-eat accounting-interval seed(-1=time(0)) sleep(u|s) print(y|n) |
The parameters are as follows:
I have a null solution in src/dphil_0_compiles.c:
#include <stdio.h> #include <pthread.h> #include "dphil.h" /* Each of these does nothing. */ void *initialize_v(int phil_count) { return NULL; } void i_am_hungry(void *v, int philosopher) {} void i_am_done_eating(void *v, int philosopher) {} |
This will compile, but it won't run correctly, because the philosophers never pick up the chopsticks. src/dphil_driver.c will detect that something is amiss, because it checks the states of the chopsticks when i_am_hungry() returns:
UNIX> bin/dphil_0_compiles 5 5 5 0 0 s y #-Philosophers: 5 0 Philosopher 0 Thinking (1) 0 Philosopher 1 Thinking (3) 0 Philosopher 2 Thinking (5) 0 Philosopher 3 Thinking (3) 0 Philosopher 4 Thinking (1) 1 Philosopher 4 Hungry 1 Philosopher 0 Hungry 1 Philosopher 4 Error -- stick 4 state should be 4, but it is -1. 1 Philosopher 0 Error -- stick 0 state should be 0, but it is -1. UNIX>
Here's the code -- very simple -- in src/dphil_1_right_left.c:
/* The only information we need is the number of philosophers. This is because we have to calculate the chopstick numbers from the philosopher id. */ typedef struct { int num; } MyPhil; /* Store the number of philosophers in the (void *). */ void *initialize_v(int phil_count) { MyPhil *p; p = (MyPhil *) malloc(sizeof(MyPhil)); p->num = phil_count; return p; } /* These are straightforward, simply picking up and putting down chopsticks, first right, then left. */ void i_am_hungry(void *v, int philosopher) { MyPhil *p; p = (MyPhil *) v; pick_up_chopstick(philosopher, philosopher); pick_up_chopstick(philosopher, (philosopher+1)%p->num); } void i_am_done_eating(void *v, int philosopher) { MyPhil *p; p = (MyPhil *) v; put_down_chopstick(philosopher, philosopher); put_down_chopstick(philosopher, (philosopher+1)%p->num); } |
You can run it and inspect the output -- this one goes through time steps zero through six in a simple example:
UNIX> bin/dphil_1_right_left 5 5 5 0 0 s y | head -n 50 | sed '/^ *[789]/,$d' > txt/output_D1_simple.txtAnd here's that output file (txt/output_D1_simple.txt), which I have annotated so that you can walk through it.
#-Philosophers: 5 0 Philosopher 0 Thinking (2) 0 Philosopher 1 Thinking (3) 0 Philosopher 2 Thinking (4) 0 Philosopher 3 Thinking (1) 0 Philosopher 4 Thinking (5) 1 Philosopher 3 Hungry | Timestep 1: 1 Philosopher 3 Picked Up Stick 3 | Philosopher 3 gets the chopsticks and eats. 1 Philosopher 3 Picked Up Stick 4 1 Philosopher 3 Eating (1) 2 Philosopher 0 Hungry | Timestep 2: 2 Philosopher 0 Picked Up Stick 0 | Philosopher 0 gets the chopsticks and eats. 2 Philosopher 0 Picked Up Stick 1 | Philosopher 3 puts the chopsticks down. 2 Philosopher 0 Eating (3) 2 Philosopher 3 Put Down Stick 3 2 Philosopher 3 Put Down Stick 4 2 Philosopher 3 Thinking (3) 3 Philosopher 1 Hungry | Timestep 3: 3 Philosopher 1 Blocking on Stick 1 | Philosopher 1 hungry: Blocks on chopstick 1. 4 Philosopher 2 Hungry | Timestep 4: 4 Philosopher 2 Picked Up Stick 2 | Philosopher 2 gets the chopsticks and eats. 4 Philosopher 2 Picked Up Stick 3 4 Philosopher 2 Eating (2) 5 Philosopher 4 Hungry | Timestep 5: 5 Philosopher 4 Picked Up Stick 4 | Philosopher 4 hungry: 5 Philosopher 4 Blocking on Stick 0 | - Gets chopstick 4, blocks on 0. 5 Philosopher 0 Put Down Stick 0 | Philosopher 0 puts the chopsticks down. 5 Philosopher 0 Put Down Stick 1 | Philosopher 4 now gets chopstick 0 and eats. 5 Philosopher 0 Thinking (1) | Philosopher 1 now gets stick 1, blocks on stick 2. 5 Philosopher 4 Picked Up Stick 0 | Philosopher 3 hungry: Blocks on 3. 5 Philosopher 4 Eating (3) 5 Philosopher 1 Picked Up Stick 1 5 Philosopher 1 Blocking on Stick 2 5 Philosopher 3 Hungry 5 Philosopher 3 Blocking on Stick 3 6 Philosopher 2 Put Down Stick 2 | Timestep 6: 6 Philosopher 2 Put Down Stick 3 | Philosopher 2 puts the chopsticks down. 6 Philosopher 2 Thinking (4) | Philosopher 1 now gets chopstick 2 and eats. 6 Philosopher 1 Picked Up Stick 2 | Philosopher 3 now gets chopstick 3, blocks on 4. 6 Philosopher 1 Eating (1) | Philosopher 0 hungry, blocks on chopstick 0 6 Philosopher 3 Picked Up Stick 3 6 Philosopher 3 Blocking on Stick 4 6 Philosopher 0 Hungry 6 Philosopher 0 Blocking on Stick 0 |
I've written up a visualizer (scripts/dphil.tcl) for the output of the programs. It is meant to be run in two ways. The first is by piping standard output of the program into the visualizer. The second is by using the program phil_step on output files. Let me show you how phil_step works:
UNIX> bin/phil_step txt/output_D1_simple.txt | wish scripts/dphil.tcl 1 Philosopher 3 Hungry 1 Philosopher 3 Picked Up Stick 3 1 Philosopher 3 Picked Up Stick 4 1 Philosopher 3 Eating (1)This will bring up a viz window, showing the philosophers in timestep 0. Philosopher 0 is on the top, and the philsopher numbers increase clockwise. I don't show the numbers -- sorry:
Phil_step displays the "next" timestep from the one that is being displayed, and when you type RETURN, it will send that information to be displayed by the viz. For example, if you type return into the command above, it will print the next timesteps, and the viz will display the current timestep. In this case, phil_step shows what will happen in timestep 2, and the viz shows what is happening in timestep 1 (philosopher 3 is eating). As you can see, "thinking" is yellow, and "eating" is blue.
2 Philosopher 0 Hungry 2 Philosopher 0 Picked Up Stick 0 2 Philosopher 0 Picked Up Stick 1 2 Philosopher 0 Eating (3) 2 Philosopher 3 Put Down Stick 3 2 Philosopher 3 Put Down Stick 4 2 Philosopher 3 Thinking (3) |
|
Here are timesteps 2, 3, 4 and 5:
Timestep 2: Philosopher 3 stops eating. Philosopher 0 gets sticks and eats. |
Timestep 3: Philosopher 1 hungry. (Hungry is red). |
Timestep 4: Philosopher 2 gets sticks and eats. |
Timestep 5: Philosopher 0 stops eating. Philosopher 1 gets stick 1, blocks on stick 2. Philosopher 4 gets sticks and eats. Philosopher 3 hungry and blocks. |
Now, this solution is not a good solution. The major reason is that it can deadlock. We'll show that in the next section. A secondary, very important reason is that philosophers spend too much time being hungry. We can demonstrate this in two ways. The first is to use the "u" option and to look at the blocktimes. Here I have the think and eat times set to random numbers up to 500 microseconds, and I print the hungry times after 20 seconds:
UNIX> bin/dphil_1_right_left 5 500 500 20000000 1 u n 20.005 Total-Hungry 70.273 20.005 Individual-Hungry 14.048 14.073 14.031 14.064 14.057 <CNTL-C> UNIX>We'll see that these aren't the best times. The other way is to look at the viz when you have a lot of philosophers. Here's an example to try:
UNIX> bin/dphil_1_right_left 25 5 5 0 0 s y | wish scripts/dphil.tclAfter a while when running this, I saw the following:
You should see why this is bad -- of the 25 philosophers, 13 of them are blocked holding their right chopstick and waiting for their left chopstick. (One is blocked, waiting for his right chopstick). Many of these philosophers could eat with a better protocol.
After timestep 2 | What happens in timestep 3:
|
After timestep 3 |
What makes this a figural example is that all of the philosophers are trying to get their right sticks, and then their left sticks. This can't happen with any of the other solutions.
UNIX> bin/dphil_1a_right_left 5 2 2 0 0 s y #-Philosophers: 5 0 Philosopher 0 Thinking (2) 0 Philosopher 1 Thinking (2) 0 Philosopher 2 Thinking (2) 0 Philosopher 3 Thinking (2) 0 Philosopher 4 Thinking (2) 2 Philosopher 0 Hungry 2 Philosopher 1 Hungry 2 Philosopher 2 Hungry 2 Philosopher 3 Hungry 2 Philosopher 4 Hungry 2 Philosopher 0 Picked Up Stick 0 2 Philosopher 1 Picked Up Stick 1 2 Philosopher 2 Picked Up Stick 2 2 Philosopher 3 Picked Up Stick 3 2 Philosopher 4 Picked Up Stick 4 5 Philosopher 0 Blocking on Stick 1 5 Philosopher 1 Blocking on Stick 2 5 Philosopher 3 Blocking on Stick 4 5 Philosopher 4 Blocking on Stick 0 5 Philosopher 2 Blocking on Stick 3 |
|
This code is in: src/dphil_2_even_odd.c, and here is the relevant code:
/* Even philosophers go right-left, and odd philosophers go left-right. */ void i_am_hungry(void *v, int philosopher) { MyPhil *p; p = (MyPhil *) v; if (philosopher % 2 == 0) { pick_up_chopstick(philosopher, philosopher); pick_up_chopstick(philosopher, (philosopher+1)%p->num); } else { pick_up_chopstick(philosopher, (philosopher+1)%p->num); pick_up_chopstick(philosopher, philosopher); } } |
When we run this on our "usleep" example above, we see much better blocking times:
UNIX> bin/dphil_2_even_odd 5 500 500 20000000 1 u n 20.005 Total-Hungry 32.079 20.005 Individual-Hungry 4.896 6.257 6.259 7.154 7.513 <CNTL-C> UNIX>Do you see anything odd with those individual block times? Philosopher 0 is hungry a lot less than the others. Philsophers 1 and 2 are hungry less than philosophers 3 and 4. Let's think about why that is true -- here is the chopstick that each philosopher tries to grab first:
Let's look at a picture of 25 philosophers running with this "solution."
UNIX> bin/dphil_2_even_odd 25 5 5 0 0 s y | wish scripts/dphil.tcl
As you can see, there are a few instances above where philosophers could eat, but aren't eating. This should give you a hint that this protocol, although it prevents deadlock, isn't the best.
After timestep 3 | What happens in timestep 4:
|
After timestep 4 |
What makes this a figural example is that you can see the odd philosophers going left-right and the even philosophers going right-left. This can't happen with any of the other solutions.
Study this code if it is not straightforward to you -- this is a pretty classic use of condition variables. Below, I show our data structure, and the pickup/putdown code:
typedef struct { int num; pthread_mutex_t *lock; /* This is so that you can look at the chopsticks. */ pthread_cond_t **blocked_philosophers; /* Block if either chopstick is not available. */ int *stick_states; /* This is how you monitor the chopsticks. */ } MyPhil; /* 'U' means taken. 'D' means available. */ void i_am_hungry(void *v, int philosopher) { MyPhil *p; int stick1, stick2; p = (MyPhil *) v; stick1 = philosopher; stick2 = (philosopher+1)%p->num; /* While either chopstick is in use, block. */ pthread_mutex_lock(p->lock); while (p->stick_states[stick1] == 'U' || p->stick_states[stick2] == 'U') { pthread_cond_wait(p->blocked_philosophers[philosopher], p->lock); } /* Now, both chopsticks are available. We hold the lock to make sure that no other philosopher gets our chopsticks while we are picking up chopsticks. */ pick_up_chopstick(philosopher, philosopher); pick_up_chopstick(philosopher, (philosopher+1)%p->num); p->stick_states[stick1] = 'U'; p->stick_states[stick2] = 'U'; pthread_mutex_unlock(p->lock); } void i_am_done_eating(void *v, int philosopher) { MyPhil *p; int stick1, stick2; int leftp, rightp; p = (MyPhil *) v; stick1 = philosopher; stick2 = (philosopher+1)%p->num; leftp = (philosopher+p->num-1)%p->num; rightp = stick2; pthread_mutex_lock(p->lock); put_down_chopstick(philosopher, philosopher); put_down_chopstick(philosopher, (philosopher+1)%p->num); p->stick_states[stick1] = 'D'; p->stick_states[stick2] = 'D'; /* After we put down out chopsticks, we need to signal adjacent philosophers. */ pthread_cond_signal(p->blocked_philosophers[leftp]); pthread_cond_signal(p->blocked_philosophers[rightp]); pthread_mutex_unlock(p->lock); } |
Now, you see much better hungry times:
UNIX> bin/dphil_3_hold_and_wait 5 500 500 20000000 1 u n 20.004 Total-Hungry 29.211 20.004 Individual-Hungry 5.814 5.862 5.842 5.855 5.837 <CNTL-C> UNIX>Here's a screen shot of the 25-philosopher case. One of the features of this is that you'll never have a case where three philsophers in a row are blocked, or that you have two adjacent blocked philosophers where one is next to a thinking philosopher.
This implementation does not prevent starvation. Consider the following sequence of thinking and eating:
Starting State: Philosophers 0 and 2 eating. |
Philosopher 2 stops eating. Philosopher 3 now eats. |
Philosopher 0 stops eating. Philosopher 1 now eats. |
Philosopher 1 stops eating. Philosopher 0 now eats. |
Philosopher 3 stops eating. Philosopher 2 now eats. Repeat; repeat; repeat. |
If the philosophers always think and eat in this fashion, then philosopher 4 starves. That's because the protocol does not do anything to make sure that philosophers eventually get the chopsticks.
After timestep 6 | What happens in timestep 7:
|
After timestep 7 |
This can't be either of the first two solutions after timestep 6, because philosopher 3 would have grabbed the right stick in solution 1, and philospher 1 would have grabbed the left stick in solution 2. In the solutions that follow (well, with the exception of the last, but we'll ignore that), philosopher 2 would not be allowed to eat in timestep 7.
Does this mean that only one philosopher can eat? No -- at all times, if the philosopher at the head of the queue can eat, he or she should eat. That means, for example, that if philosophers 0, 2 and 1 become hungry in that order, 0 and 2 can eat. On the flip side, if philosophers 0, 1 and 2 become hungry in that order, only 0 can eat.
To implement this, we need to check the head of the queue at three times:
Here is the relevant code:
/* Now we keep a queue of philosophers who are hungry. If the queue isn't empty, then whenever a philosopher is hungry, he/she has to go on the end of the queue. Only the philosopher on the head of the queue can eat. We use a simple array to model the queue. The queue's size is held in wq_size. The first philosopher is at wq_head. When you insert a philosopher on the queue, then he/she goes into elelemtn (wq_head+wq_size)%num. */ typedef struct { int num; pthread_mutex_t *lock; pthread_cond_t **blocked_philosophers; int *stick_states; int *waiting_queue; int wq_head; int wq_size; } MyPhil; void i_am_hungry(void *v, int philosopher) { MyPhil *p; int stick1, stick2; p = (MyPhil *) v; stick1 = philosopher; stick2 = (philosopher+1)%p->num; pthread_mutex_lock(p->lock); /* Put yourself on the queue */ p->waiting_queue[(p->wq_head + p->wq_size)%p->num] = philosopher; p->wq_size++; /* You can't eat until you are on the head of the queue, and your chopsticks are available. */ while (p->waiting_queue[p->wq_head] != philosopher || p->stick_states[stick1] == 'U' || p->stick_states[stick2] == 'U') { pthread_cond_wait(p->blocked_philosophers[philosopher], p->lock); } /* When you reach this part of the code, you can eat. */ pick_up_chopstick(philosopher, philosopher); pick_up_chopstick(philosopher, (philosopher+1)%p->num); p->stick_states[stick1] = 'U'; p->stick_states[stick2] = 'U'; /* Remove yourself from the queue, and signal whoever is now on the head of the queue. */ p->wq_head = (p->wq_head + 1) % p->num; p->wq_size--; if (p->wq_size > 0) { pthread_cond_signal(p->blocked_philosophers[p->waiting_queue[p->wq_head]]); } pthread_mutex_unlock(p->lock); } /* When you're done eating, signal the philosopher on the head of the queue. */ void i_am_done_eating(void *v, int philosopher) { MyPhil *p; int stick1, stick2; int leftp, rightp; p = (MyPhil *) v; stick1 = philosopher; stick2 = (philosopher+1)%p->num; leftp = (philosopher+p->num-1)%p->num; rightp = stick2; pthread_mutex_lock(p->lock); put_down_chopstick(philosopher, philosopher); put_down_chopstick(philosopher, (philosopher+1)%p->num); p->stick_states[stick1] = 'D'; p->stick_states[stick2] = 'D'; if (p->wq_size > 0) { pthread_cond_signal(p->blocked_philosophers[p->waiting_queue[p->wq_head]]); } pthread_mutex_unlock(p->lock); } |
This does prevent starvation, but as you can imagine, it leads to much larger hungry times:
UNIX> bin/dphil_4_global_list 5 500 500 20000000 1 u n 20.000 Total-Hungry 44.459 20.000 Individual-Hungry 8.882 8.914 8.884 8.891 8.889 <CNTL-C> UNIX>The screenshot is from a run with 25 philosophers, and as you can see, we have a lot of blocked philosophers here -- if the head of the queue is blocked, then everyone can be blocked.
After timestep 35 | What happens in timestep 36:
|
After timestep 36 |
This is the only solution where this would happen -- a philosopher becomes hungry, both adjacent philosophers are thinking, and the philosopher blocks.
The implementation is in src/dphil_5_take_a_number.c. What I do here is associate a number with each philosopher. When the philosopher is thinking, his/her number is a high sentinel (0x7fffffff). When the philosopher is eating, his/her number is a low sentinel (-1). And when a philosopher becomes hungry, he/she gets a number from a counter, that is increased after the philosopher gets the number.
Under this system, a philosopher can only eat if he/she has a lower number than his/her neighbors. Very simple. If a philosopher can't eat, he/she blocks on a conditional variable, when when a philosopher stops eating, he/she signals his/her neighbors.
Here's the relevant code:
/* No more queue here -- each philosopher is going to "take a number" when hungry. */ typedef struct { int num; pthread_mutex_t *lock; pthread_cond_t **blocked_philosophers; int *phil_number; int counter; } MyPhil; #define HIGH_SENTINEL (0x7fffffff) #define LOW_SENTINEL (-1) void i_am_hungry(void *v, int philosopher) { MyPhil *p; int leftp, rightp; p = (MyPhil *) v; leftp = (philosopher+1)%p->num; rightp = (philosopher+p->num-1)%p->num; pthread_mutex_lock(p->lock); /* Here's where I take a number. */ p->phil_number[philosopher] = p->counter; p->counter++; /* As long as my neighbors have a lower number than me, block. */ while (p->phil_number[leftp] < p->phil_number[philosopher] || p->phil_number[rightp] < p->phil_number[philosopher]) { pthread_cond_wait(p->blocked_philosophers[philosopher], p->lock); } pick_up_chopstick(philosopher, philosopher); pick_up_chopstick(philosopher, (philosopher+1)%p->num); p->phil_number[philosopher] = LOW_SENTINEL; pthread_mutex_unlock(p->lock); } void i_am_done_eating(void *v, int philosopher) { MyPhil *p; int leftp, rightp; p = (MyPhil *) v; leftp = (philosopher+1)%p->num; rightp = (philosopher+p->num-1)%p->num; pthread_mutex_lock(p->lock); put_down_chopstick(philosopher, philosopher); put_down_chopstick(philosopher, (philosopher+1)%p->num); p->phil_number[philosopher] = HIGH_SENTINEL; pthread_cond_signal(p->blocked_philosophers[leftp]); pthread_cond_signal(p->blocked_philosophers[rightp]); pthread_mutex_unlock(p->lock); } |
When we run this, we don't see much improvement over the global queue.
UNIX> bin/dphil_5_take_a_number 5 500 500 20000000 1 u n 20.000 Total-Hungry 41.099 20.000 Individual-Hungry 8.232 8.191 8.211 8.222 8.243 <CNTL-C> UNIX>The global queue had a total hungry time of 44.459 seconds. The hold-and-wait solution's total hungry time was 29.211. That seems dire, but try the viz. You won't have to wait too long until you get something like this:
As you can see, philosopher 1 is blocking, even though the chopsticks are available. With just five philosophers, there aren't a huge number of cases where this implementation is better than the global queue. If you run them both with a larger table, the difference is more marked:
UNIX> bin/dphil_4_global_list 25 500 500 20000000 1 u n 19.999 Total-Hungry 361.248 19.999 Individual-Hungry 14.408 14.460 14.440 .... <CNTL-C> UNIX> bin/dphil_5_take_a_number 25 500 500 20000000 1 u n 20.001 Total-Hungry 207.315 20.001 Individual-Hungry 8.308 8.287 8.326 ... <CNTL-C> UNIX>Here's my 25-philosopher screen shot. As you can see, this protocol does not prevent multiple adjacent philosophers from blocking. You can get chains of blocking philosophers, depending on the order in which they get hungry.
After timestep 2 | What happens in timestep 3:
|
After timestep 3 |
You can identify this algorithm with three facts:
The last solution is just like the previous one, but instead of blocking if your neighbor has been hungry longer than you, you block when your neighbor is really hungry. In the code -- you block when your neighbor's number is more than 500 less than your number. You need to be careful with the "Eating" sentinel -- if you make it -501, and you start your counter at 0, then it works properly.
I'm only going to include the changed line in i_am_hungry(). The program is src/dphil_6_hybrid.c:
#define THRESH (5) ... while (p->phil_number[philosopher] - p->phil_number[leftp] > THRESH * p->num || p->phil_number[philosopher] - p->phil_number[rightp] > THRESH * p->num) { pthread_cond_wait(p->blocked_philosophers[philosopher], p->lock); } |
Now, the performance is like the Hold-and-Wait implementation, but starvation is prevented!
UNIX> bin/dphil_6_hybrid 5 500 500 20000000 1 u n 19.999 Total-Hungry 29.107 19.999 Individual-Hungry 5.802 5.785 5.834 5.822 5.864 <CNTL-C> UNIX>You'll see that the performance is superior to the previous two with 25 philosophers too:
UNIX> bin/dphil_6_hybrid 25 500 500 20000000 1 u n 20.004 Total-Hungry 155.495 20.004 Individual-Hungry 6.240 6.168 6.241 ... <CNTL-C> UNIX>
A few things of note: