Dining Philosophers

The dining philosophers problem is a ``classical'' synchronization problem
first posed by Edsger Dijkstra.
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.

The book (again, chapter 6) 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:

If these philosophers are so smart, shouldn't they be worried about
communicable diseases?

Why chopsticks? For some reason I envision philosophers liking soup.

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.

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
spent waiting to eat.

In case you're bored, here is that last paragraph in the
inimitable words of professor Wolski:
``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 CBThreads

What I've done is hack
up a general driver for the dining philosophers problem using CBThreads,
and then implemented several "solutions". Thus, it is structured like your
labs. The main simulation is called with the following parameters:

nphilosophers: The number of philosophers.
The typical setting is 5, but the problem becomes more interesting
when the number of philosophers is a big number.

thinkavg: The average time that a philosopher thinks. These will be generated
according to a uniform distribution from zero to thinkavg*2.

eatavg: The average time that a philosopher eats. These will be generated
according to a uniform distribution from zero to eatavg*2.

sticktime: The time that it takes a philosopher to pick up a chopstick. This
is also the time that it take a philosopher to put down a chopstick. In the
canonical specification of the problem, there is no "sticktime," but it makes the
problem more interesting and challenging.

interval: This will print out statistics at a given interval of time.

duration: This is the duration of the simulation. At the end of the simulation,
statistics are printed.

seed: Seed for srand48().

verbose: If yes, this will print out the philosopher's actions. Otherwise, only
the statistics are printed.

The driver is pretty straightforward. It reads the simulation parameters and
creates a Philosopher struct for each philosopher. All the philosophers
are available in the p array of the Simulation struct. Before
forking any threads, it calls initialize_simulation() so that you can
add any state that you want in the v field.

Then it forks off the philosopher threads.

The philosopher threads of course have to use continuations when they block, and
what we do is always call philosopher() as our continuation. Philosopher()
checks the state of the philosopher and does the correct thing. There are
six states. Here is what philosopher() does in each:

STARTING: Calculate a random thinking time, set the state to THINKING
and sleep for that time (all sleeping is "fake").

THINKING: Set the state to HUNGRY and call i_am_hungry(), which
you define. I_am_hungry() is blocking. When it unblocks it should call philosopher(p),
the philosopher's state should be GOTSTICKS, and the philosopher should have picked up the
chopsticks using pick_up_stick().

GOTSTICKS: Calculate a random eating time, set the state to EATING
and sleep for that time (all sleeping is "fake").

EATING: Set the state to SATED and call i_am_sated(), which
you define. I_am_sated() is also blocking. When it unblocks it should call philosopher(p),
the philosopher's state should be STARTING, and the philosopher should have put down the
chopsticks using put_down_stick().

Each Philosopher struct keeps statistics on how long the philosopher has been thinking, eating and blocked.
These statistics are maintained in the procedures philosopher() and update_times(). They are
straightforward and I won't explain them further.

pick_up_stick(Philosopher *p, int stick, void (*func)()): You are to call this when a philosopher wants
to pick up the specified chopstick (numbered 0 through nphil-1). This procedure double-checks to make sure
that the chopstick is free, and then sleeps for sticktime seconds. When it awakes, it calls the
given continuation function on p.

put_down_stick(Philosopher *p, int stick, void (*func)()): This is just like pick_up_stick(),
except the puts the stick down.

What you define

You define initialize_simulation(), i_am_hungry() and i_am_sated(). Their definitions are above,
but I'll restate them:

initialize_simulation(Simulation *s): This allows you to set up s->v any way you want.

i_am_hungry(Philosopher *p): The philosopher's state will be HUNGRY. You need to write this so
that it never returns. Instead, it will call philosopher(p) with the following:

The philosopher's state will be GOTSTICKS.

The philosopher will have called pick_up_stick() on sticks p->id and
(p->id+1)%p->s->nphil.

i_am_sated(Philosopher *p): The philosopher's state will be SATED. You need to write this so
that it never returns. Instead, it will call philosopher(p) with the following:

The philosopher's state will be STARTING.

The philosopher will have called put_down_stick() on sticks p->id and
(p->id+1)%p->s->nphil.

To be successful, your implementation will guarantee that two adjacent philosophers do not pick up the same
chopstick. It should also have the following properties:

The philosophers should not deadlock (where each holds one chopstick and is waiting for the other).

No philosophers should starve (when one is hungry, he will eventually be able to eat).

It should be fair (no philosopher should be favored).

It should be as efficient as possible (the philosophers' blocking times should be minimized).

Solution #1: The Null Solution

The first solution is to have all three procedures do nothing. It is in
dphil_1.c:

Of course it compiles, but it doesn't run correctly. Here's an example where the philosophers think and eat
for an average of three seconds, and it takes one second to pick up and put down the chopsticks:

The first philosopher to call i_am_hungry() is philosopher 2, and since i_am_hungry() just
returns, that is flagged as an error.

Solution #2: Just try to grab those sticks

The second solution, in
dphil_2.c
simply has each philosopher just try to grab the chopsticks. Since pick_up_sticks()
and put_down_sticks() are blocking, this needs to be continuation based, and to
do that, I add a second state for each philosopher. This is in the array my_states,
which is held in s->v. So, initialize_simulation() allocates the array and
sets all states to BEGIN. Then i_am_hungry() works according to the states:

If the state is BEGIN, set the state to GOT_STICK_1 and call
pick_up_stick() on stick p->id.

If the state is GOT_STICK_1, set the state to GOT_STICK_2 and call
pick_up_stick() on stick (p->id+1)%p->s->nphil.

If the state is GOT_STICK_2, then we're done. Set the state to BEGIN,
set p->state to GOTSTICKS and call philosopher(p).

Philosopher's 0 and 2 are the first to wake up, and they successfull pick up their chopsticks and
start eating. By luck, philosopher 4 is the next to wake up, and he picks up stick 4, which is
the only stick available. However, when he tries to pick up stick 0, we get an error.

Although this solution is a bad one, there are times when it will work. For example, make the think times
big and the eat/stick times small:

Solution #3: Protect solution 2 with semaphores

The obvious way to implement mutual exclusion is with a semaphore initialized to one.
dphil_3.c does this by adding a semaphore to every
chopstick, and now the philosophers must call P() before calling pick_up_stick()
and V() after calling put_down_stick(). Yes, continuations make this a pain,
but by structuring the code around the philosopher's my_state, it's pretty straighforward.
I'm only showing initialize_simulation() and i_am_hungry():

Solution #4: Asymmetry

One way to fix this problem is with asymmetry -- don't have every philosopher start with
his or her left chopstick. A simple way to implement this is to have even numbered
philosonphers start with chopstick p->pid and odd ones start with (p->pid+1)%p->s->nphil.
In this way, you do not have circular waiting. The proof is fairly simple: With the exception
of philospher zero, suppose a philosopher
holds one chopstick but can't get the other. This means that the philosopher holding the
other chopstick must be eating, and when that philosopher is done, the chopstick will be
available. You can make similar proof about philosopher zero.

We implement this in dphil_4.c. The only change to the code is
in the beginning of i_am_hungry():

Indeed, philosopher 0 is getting the best of all worlds -- she is blocked less, and
therefore gets to eat more and think more. On the flip sides, philosohpers 9 and 10
seem to be getting a raw deal. It actually makes sense -- whenever philosopher 0
has to wait for a chopstick, it's because the adjacent philosopher is eating.
Contrast that with philosopher 1. When that philosopher waits for chopstick 0,
it may be the case that philosopher 0 is holding it and waiting for chopstick 10.
That will translate to a longer wait time than philosopher 0's wait times.

The asymmetry prevents the deadlock, but unfortunately, it also causes unfairness.
This will of course go away with an even number of philosophers, but with an
odd number, philosopher 0 gets an unfair advantage.

Solution #5: The Book's Solution

The book's solution prevents deadlock and is fair. The algorithm is simple:
only pick up the chopsticks when both are available. To implement this
(dphil_5.c),
we add an extra array to our system -- one that says whether the chopsticks
are free. We have the same semaphores, but now we assign them to philosophers
rather than to chopsticks.
When a philosopher is hungry, he/she checks this array, and if
both chopsticks are free, the philosopher sets them both as used, and then
goes through the activity of picking them up. If the chopsticks aren't
free, the philosopher sets his/her state to blocked and waits on his/her semaphore.
We'll see how the philosopher is awakened in a bit. Here's the code for
i_am_hungry():

Now, when a philosopher puts down a chopstick, he/she checks to see if the adjacent
philosopher is blocked and if so, sets the philosopher's state back to BEGIN
and wakes him/her up. It may not be the case that the philosopher can get both
sticks, but the philosopher will check that upon waking up:

You may wonder -- why do I set an adjacent philosopher's state to BEGIN when
calling cbthread_gsem_V()? The answer is subtle -- suppose philosophers 0
and 2 both stop eating simultaneously, with philosopher 0 executing first. If philosopher
0 didn't set philosopher 1's state to BEGIN when calling cbthread_gsem_V(),
then philosopher 2 would also call cbthread_gsem_V() before philosopher 1
wakes up. That's wouldn't be good.

When we run it, we get a better blend: the philosophers eat more or less equally, and
there's no deadlock:

It doesn't perform as well as dphil_4 though. We'll discuss why later. It also
has a very minor issue in that it can exhibit starvation. Consider the following sequence:

Philosopher 0 is hungry and gets the chopsticks.

Philosopher 4 is hungry and waits.

Philosopher 2 is hungry and gets the chopsticks.

Philosophers 1 and 3 are hungry and wait.

Philosopher 2 is sated and puts down the chopsticks.

Philosopher 3 now gets the chopsticks.

Philosopher 0 is sated and puts down the chopsticks.

Philosopher 1 now gets the chopsticks.

Philosophers 0 and 2 are hungry and wait.

Philosopher 1 is sated and puts down the chopsticks.

Philosopher 0 now gets the chopsticks.

Philosopher 3 is sated and puts down the chopsticks.

Philosopher 2 now gets the chopsticks.

Philosophers 1 and 3 are hungry and wait.

Repeat from step 5

The process is illustrated below. Note how philosopher 4 is never able to get the chopsticks
because both are never available:

In dphil_skeleton_skew.c, I've modified the driver so that
all the philosophers but philosopher 0 think for an extra second, philosophers 1 and 2
eat for an extra second, and the rest eat for an extra three seconds. When you call it with
zero values for the think, eat and stick times, you get the scenario pictured above, and
philosopher 4 starves:

Solution #6: Preventing Starvation

One way to ensure no starvation is to enforce an ordering on when the philosophers
eat. A naive implementation of this is to use a queue. When a philosopher wants
to eat, he/she gets appended to the queue. Then a philosopher can only eat if
he/she is at the front of the queue with the chopsticks free.

The implementation is straightforward again -- I've added a Dllist and
a stated called QUEUED. Moreover, I've implemented a procedure called
test_philosopher() which philosophers may use to test whether they or
adjacent philosophers can eat (in dphil_6.c):

The progression is pictured below. The important feature is that since the philosopher only
check adjacent philosophers, you have a problem when the front of the queue is not an adjacent
philosopher:

Solution #7: Preventing Starvation A Little Better

We can fix this easily by testing the front of the queue instead of testing adjacent
philosophers. The fix is in dphil_7.c. First, we write a procedure
that tests the front of the queue and wakes up the philosopher:

Solution #9: Central queues are inefficient

When the table gets large, a central queue seems really inefficient, doesn't it? Suppose
that the table size is 100, all philosophers are hungry, philosopher 0 is eating and
philosopher 1 is at the head of the queue. Then there are 49 philosophers that can
eat, but none will be able to until philosopher 0 is done.

So, can we prevent starvation without a central queue? Sure -- one way is to have
each philosopher keep track of what time they became hungry. Then a philosopher can
only eat when the chopsticks are available and neither neighbor has been hungry
for a longer period of time.

The code is in
dphil_9.c. The only changes are that we've added a
hunger_time array to MyPhil, and our test_philosopher()
routine now tests adjacent philosophers instead of a central queue:

Why would it perform worse on a 5-person table? For a hint, take a look at the
scenario below -- hunger times are next to the philosophers:

That's no better than a central queue, is it? And with a table that small, you are very likely
to have situations like the above.

Solution #A: Loosening the hunger time requirement

We can fix the above by loosening the restriction. How about we set a threshold, and you
only block if the difference between your hunger time and your neighbor's is above that
threshold. Then you still prevent starvation, but you won't have as much blocking.

This is in
dphil_a.c, and is a two-line fix to test_philosopher(),
where we use a threshold of eatavg * 5.

Why? The answer is that all of the solutions from #5 on don't start picking up
chopsticks until both are available. Now, suppose your left chopstick is available
and the philosopher on your right is putting down his left chopstick. You can
improve matters by starting to pick up your left chopstick now instead of waiting
until your right chopstick is available.

This is done in
dphil_b.c. It's a little tricky, because you have to
worry about things that can happen when two philosophers wake up at the same
time (which happens quite a bit as it turns out). I'm not going to put the
code here, but I represent the state of each chopstick as FREE, USING,
ALLOCATED or DROPPING. They are all pretty obvious except for
ALLOCATED -- this means that you are picking up the other chopstick and
this chopstick is not in use, but you are going to use it.

Now test_philosopher() has more things to worry about -- if either
of your sticks are USING or ALLOCATED, then you must block.
If both sticks are DROPPING you also must block. Otherwise, you can
pick up a stick. I return the number of free sticks:

Solution #C: I can't help myself...

You can make the last solution even more efficient if you are judicious about the
order in which you put down your chopsticks. If putting down one chopstick will enable a
philosopher to start picking up the other chopstick, then put that chopstick down first.
This improves performance even more:

In Summary

Here are some graphs to compare the various methodologies. The graphs that I present
plot averages of ten runs with different seeds, where each duration is 100,000 seconds.
I plot the percentage of time that each philosopher is blocked as a function of
the table size (number of philosophers):

Thinktime=3, Eattime=3, Sticktime=1

Thinktime=3, Eattime=9, Sticktime=1

These first two simulations are similar. Both represent states where there is always
contention for the chopsticks. In each case, the greedy solution is the worst, as you
get lots of philosophers holding a chopstick and waiting. The second worst algorithm is
the central queue, which as noted above gets worse as the table gets bigger.

The remainder of the algorithms don't appear to depend on the table size. In the first
case, where the thinking and eating times are similar, the asymmetric algorithm
works better than all but the last, because it doesn't make the philosophers wait until
both chopstricks are free before picking a chopstick up.

Below, we show some more examples:

Thinktime=9, Eattime=3, Sticktime=1

Thinktime=3, Eattime=3, Sticktime=0

What's going on with the Greedy Solution when the table size is 5?
Think about it before looking at the answer.

What's happening is that in nine of the ten cases, the philosophers deadlock!
In the first two sets of tests, they don't. Is that intuitive or counter-intuitive?
I think the latter -- If you're thinking more, you're less
likely to have contention and therefore you're less likely to deadlock.

But that's not the case. In the first two examples, the philosophers are usually hungry
or eating. When they're hungry, they are usually blocked waiting for adjacent philosophers
to finish, and it is rare for adjacent philosophers to get hungry at the same time.
In other words, the contention makes the philosophers eat in a rough pattern, and
that pattern prevents deadlock.

When the thinking time is larger, the philosophers are less likely to contend with
each other, so they don't get into this pattern of alternating eating. Instead,
each starts at a random time. If you choose these random times enough, you are more
likely to get that combination where all the philosophers get hungry within a second
of each other. Odd, but true. When there are 10 philosophers, that probability is
simply too low.

When we remove the sticktime from the equation, solutions 5, A and C all become
equivalent.