C360 Lecture notes -- Dining Philosophers

  • James S. Plank
  • CS360
  • Directory: /home/plank/cs360/notes/Dphil
  • Lecture notes: http://web.eecs.utk.edu/~jplank/plank/classes/cs360/360/notes/Dphil/lecture.html
  • My original Dining Philosopher's lecture was written in the late 1990's.
  • Complete overhaul: April, 2016
  • Latest modification: Mon May 2 12:21:13 EDT 2022

    Dining Philosophers

    (This lecture was originally developed for an Operating Systems class, so when you see references to books and labs, they are for that old class. When I talk about "The Book", I'm talking about Operating Systems Concepts by Silberschatz & Galvin, probably one of those middle editions like 7. You don't need to own the book to understand the lecture.)

    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:

    1. If these philosophers are so smart, shouldn't they be worried about communicable diseases?
    2. Why chopsticks? For some reason I envision philosophers liking soup.
    3. Evidently conversing isn't in here -- why do they need to be at the same table? I'm not sure if I'd enjoy having a philosopher philosophize while I'm eating. But then again, I'm not a philosopher.
    4. Shouldn't bathing be in the equation somewhere?

    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).'')


    Dining Philosophers Testbed with pthreads

    What I've done is hack up a Dining Philosophers testbed. I've organized it like a lab: You call the driver as follows:

    dphil num-philosophers max-think max-eat accounting-interval seed(-1=time(0)) sleep(u|s) print(y|n)
    

    The parameters are as follows:


    Solutions to the dining philosophers

    I have written up 8 "solutions" to this problem. You can copy all of the .c files and the makefile to your own directory and try them out for yourself. I have also written a tcl/tk program named scripts/dphil.tcl, which visualizes the output. I'll give an example of that in the next section.

    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> 
    

    Solution #1: (Right-Left) - Simply grabbing chopsticks

    The simplest solution is to have each philosopher just call pick_up_chopstick() on his/her right chopstick, and then on the left chopstick. The right one has the same id as the philosopher, and the left one is numbered (id+1)%num_philosophers (that may seem counter-intuitive, but if the philosophers are numbered clockwise around the table, that's how it happens).

    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.txt
    
    And 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.tcl
    
    After 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.


    Walk Through For Class

    For these solutions, I'm going to additionally have a walk-through of some figural examples. These are examples which are unique to the solution, and you should be able to tell that by looking at the examples. The following one comes from tracing through txt/output_9_1_right_left.txt with the visualizer. In particular, here are pictures of step 2, what happens in step 3, and step three:

    After timestep 2

    What happens in timestep 3:

    • Philosopher 4 stops eating.
    • Philosopher 1 becomes hungry, but has to block on the right stick.
    • Philosopher 7 becomes hungry, grabs the right stick but blocks on the left stick.
    • Philosopher 6 becomes hungry, grabs the right stick but blocks on the left stick.
    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.


    Solution 1A -- Deadlock

    With solution 1A, (src/dphil_1a_right_left.c) I have added a three second delay after grabbing the right chopstick. You can see deadlock here if you set the think time under two seconds:

    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
    


    Solution #2: (Even-Odd) - Even and Odd philosophers do different things

    One simple way to prevent deadlock is to have even-numbered philosophers get their chopsticks in one order, and odd-numbered philosophers get their chopsticks in the other order. That prevents the "circular wait" requirement of deadlock.

    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: Now do you see why Philosopher 0 is hungry less than the others? It's because he/she doesn't compete with another philosopher for the first chopstick. If Philosopher 0 is blocked on chopstick 0, it's because philosopher 4 is eating. All of the other philosophers can block on their first chopsticks, and the philosopher on whom they are blocking may not be eating.

    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.


    Walk Through For Class

    The following one comes from tracing through txt/output_9_2_even_odd.txt with the the visualizer. Here are pictures of step 3, what happens in step 4, and step 4:

    After timestep 3

    What happens in timestep 4:

    • Philosopher 3 becomes hungry, and blocks on the right stick.
    • Philosopher 7 becomes hungry, and blocks on the left stick.
    • Philosopher 1 becomes hungry, gets the left stick, but blocks on the right stick.
    • Philosopher 2 becomes hungry, and blocks on the right stick.
    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.


    Solution #3: (Hold and Wait) - Don't grab any chopsticks unless they are both available.

    This is the solution proposed by Silberschatz and Galvin in their textbook. It addresses the "hold and wait" requirement of deadlock -- make it so that no philosopher can pick up a chopstick unless both of his/her chopsticks are available. We implement this in src/dphil_3_hold_and_wait.c, with one mutex and one condition variable for each philosopher. The philosophers now check both chopsticks, and if either is not available, the philosopher blocks on his/her condition variable. This requires philosophers to signal adjacent philosophers when they put their chopsticks down.

    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.


    Walk Through For Class

    The following one comes from tracing through txt/output_9_3_hold_n_wait.txt with the the visualizer. Here are pictures of step 6, what happens in step 7, and step 8:

    After timestep 6

    What happens in timestep 7:

    • Philosopher 2 becomes hungry, gets both sticks and starts eating.
    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.


    Solution #4: (Global List) - You can't eat if someone before you is hungry.

    We now start to address starvation. In order to do so, we need to ensure that if a philosopher is hungry, he or she eventually eats. The simplest of these is in src/dphil_4_global_list.c. In this implementation, we maintain a global queue, and whenever a philosopher is hungry, he/she goes onto the end of the queue. Our invariant is that the only philosopher who can eat is the one on the head of the queue.

    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:

    1. Whenever a philosopher becomes hungry.
    2. Whenever a philosopher stops eating.
    3. Whenever a philosopher starts eating.
    The last of these happens in the following situation -- philosophers 1, 0 and 2 become hungry in that order. Philosopher 1 will eat, and when he/she is done, philosopher 0 is on the head of the queue and will eat next. We need to check the queue here too, because philosopher 2 can also eat. If we didn't check the queue, then philosopher 2 would stay there, even though he/she can eat.

    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.


    Walk Through For Class

    The following one comes from tracing through txt/output_9_4_global_list.txt with the the visualizer. Here are pictures of step 35, what happens in step 36, and step 36:

    After timestep 35

    What happens in timestep 36:

    • Philosopher 4 becomes hungry and blocks.
    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.


    Solution #5: (Take A Number) - You can't eat if your neighbors are hungrier.

    An obvious problem with the last solution is that a hungry philosopher across the table from you can prevent you from eating. So, here's a more localized solution. You can never eat if either of your neighbors is hungrier than you. Now, you're only limited by your neighbors (kind of...).

    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.


    Walk Through For Class

    This comes from tracing through txt/output_9_5_take_a_number.txt steps 2 and 3: step three:

    After timestep 2

    What happens in timestep 3:

    • Philosopher 1 becomes hungry and blocks.
    • Philosopher 6 becomes hungry, gets the sticks and eats.
    After timestep 3

    You can identify this algorithm with three facts:

    1. Philosopher 1 is hungry with both sticks available, but blocks. This wouldn't happen in solutions 1-3, because philosopher 1 would get both sticks in this case.
    2. So, this has to be algorithm 4 or 5.
    3. Philsopher 6 becomes hungry and gets the chopsticks, even though philosopher 2 has been waiting longer. So this can't be solution 4. It has to be solution 5.

    Solution #6: (Hybrid) - You can't eat if your neighbors are starving.

    The problem with the previous two solutions is that they are too proactive -- they block philosophers too much to prevent starvation. Let's put it in another way: Starvation happens very rarely, but we go into very active measures to prevent it. What if we were more lax.

    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> 
    

    Timing Comparison

    Here is the comparison of the six implementations with a five-philiospher table, and with a 25-philosopher table, over 20 seconds.

    A few things of note:


    So, what are the lessons?