Many thanks to Chris Palmer for his contribution to today's notes. -GMK
Reading
Chapter 7
A Few More Words About Monitors
Three Different Types of Monitors
If P does X.wait and Q does X.signal:Hoare Monitors - Q blocks and P runs
Mesa Monitors - Q continues, P runs eventually (after Q) BH Monitors - Q can only signal as it exits, P runsCaution with Mesa Monitors
Mesa monitors do not ensure that the conditions that were true when a process or thread is signaled by another remain true at the time that it runs.To shield ourselves from this situation, we might need to change our entry test from an "if" statement to a "while" statement:
if (something) X.wait --> while (something) X.waitBut this allows for starvation. It could be the case that each time a previously blocked process gets to run, the conditions have changed and it is forced to block again.
Consider the example below:
MONITOR prod-con { struct something buffer[n]; int in=0, out=0; int count=0; condition full, empty; entry add_item (data) { if/while (count == n) full.wait; buffer[in] = data; in = (in + 1 ) % n; count++; empty.signal; } entry remove_item (data_ptr) { if/while (count == n) empty.wait; *data_ptr = buffer[out]; out = (out + 1) % n; count--; full.signal; } }This example is correct for Hoare monitors using the if, but correctness requires that this become a while under Mesa semantics. This change introduces the possibility for starvation.
Student Question: Why would we ever want Mesa monitors?
Answer: They are more efficient, sicne we can just move the signaled thread from the blocked to the ready queue. And since they involve fewer context switches, they are often more efficient.
Dining Philosophers Problem
The Dining Philosophers Problem is a classic example in computer
science. Consider a round table populated by five philosophers. Each
philosopher has a plate. In-between each plate is a fork. At the center
of the table is a bowl of spaghetti.
The rules of the problem are these:
Note: With 5 people, it is impossible for three philosophers to eat concurrently, because there are only 5 forks (2.5 pairs).
Semaphore-based Solution
#define left(i) (i) #define right(i) ((i-1) % 5) semaphore fork[5] = {1, 1, 1, 1, 1}; semaphore table = 4; while (1) { << think >> P(table); P(fork[left(i)]); P(fork[right(i)]); << eat >> V(fork[right(i)]); V(fork[left(i)]); V(table); }
But this solution has a problem: deadlock. If everyone picks up the left fork before anyone picks up the right fork, deadlock occurs. So how do we fix this problem?
Solution #1: If we require that the philosophers always pick up
their left fork before picking up their right fork,
deadlock is avoided.
Broken Suggestion: Pick up a random fork first. Well, the random forks
could all be the left fork, so deadlock is still
possible.
Broken Suggestion: If you can't pick up the right fork, put down the
left fork and try again. Well, this can lead to
livelock, where each philosopher picks up their
left fork, looks for the right fork, puts down the
left fork, and repeats.
Solution #2: Make certain thjat only 4 philosophers are trying to eat
at any time. This ensures that at least one philosopher
will be able to eat.
Solution #2 with Monitors
We can solve the deadlock and livelock problems and implement Solution #2 using a monitor. But this solution allows for startvation.
MONITOR fork { int avail[5] = {2, 2, 2, 2, 2}; // # of forks available to each philosopher condition ready[5]; entry pickup_forks (i) { if (avail[i]) != 2) read[i].wait; avail[left(i)]--; avail[right(i)]--; } entry putdown_forks(i) { avail[left(i)]++; avail[right(i)]++; if (avail[left(i)] == 2) ready[left(i)].signal; if (avail[right(i)] == 2) ready[right(i)].signal; } }
Another approach using a monitor follows. It also suffers from starvation, but it is interesting, becuase it shows the relationship between semaphores and monitors.
MONITOR dining_P { int state[5] = {thinking, thinking, thinking, thinking, thinking}; condition phil_cond[5]; entry pickup_forks (i) { state[i] = hungry; test_forks(i); if (state[i] != eating) phil_cond[i].wait; } entry putdown_forks(i) { state[i] = thinking; test_forks(LEFT); test_forks(RIGHT); } test_forks (i) { if ( (state[LEFT] != eating) && (state[RIGHT] != eating) && (state[i] == hungry) { state[i] = eating; phil_cond[i].signal; } } }
For comparison, let's look at a very similar solution using semaphores:
semaphore mutex = 1; semaphore phil_sem[5] = {0, 0, 0, 0, 0}; pickup_fork(i) { P(mutex); state[i] = hungry; test_forks(i); V(mutex); P(phil_sem[i]); } putdown_forks(i) { P(mutex); state[i] = thinking; test_fork (LEFT); test_fork(RIGHT); V(mutex); } test_forks (i) { if ( (state[LEFT] != eating) && (state[RIGHT] != eating) && (state[i] == hungry) { state[i] = eating; V(phil_sem[i]); } }
Why Do We Keep Getting Starvation?
We keep getting starvation, because each philosopher is waiting for a
different condition, so we can't ensure fairness. There are two fixes:
make all philosophers wait for a common condition, or maintain an explicit
queue of waiting philosophers to force fairness.
Deadlock
UNIX lets things deadlock. Users will realize that something has gone wrong
and explicitly kill a process. Hopefully the process that is killed will
break the dependency chain and allow the other processes to continue. If
not, the user likely will kill another process, until the problem goes
away.
Examples of deadlocks include the now familiar case of "holding one fork while waiting for another", performing a P() on a semaphore and forgetting to perform a V(), and traffic "grid lock."
A Model To Talk About Deadlock
We need a model to help us speak about deadlock.