A point that's easy to miss in a discussion of syncronization primitives is that having the primitives doesn't make the hard problems go away. It just gives you the tools to solve them. One of the classic problems, the Dining Philosphers Problem, illustrates this point really well.
The problem is stated as follows: there are five philosophers seated around a table, alternately philosophizing and eating. The food they are eating is spaghetti; it takes two forks to eat spaghetti. However, there are only five forks, so the spaces around the table alternate philosphers and forks. To eat, philospher i needs to pick up forks i and (i + 1) mod 5.
Notice that while the problem is state in a humorous way, there is a serious point to it: you've got shared resources that require exclusive access, and each process needs to get access to a specific set of resources. Humorous problem statements are a tradition in operating systems.
We'll start by simply translating the problem statement into C. The code for each philosopher is identical; the forks will be represented by an array of binary semaphores.
(confusingly, picking up a fork requires a
void philospher(int me)
{
while (1) {
philosphize();
down(fork[me]);
down(fork[(me+1) % 5];
eat();
up(fork[me]);
up(fork[(me+1) % 5];
}
}
down
semaphore operation and vice versa)
So, why is this a failed solution? Because it's possible (even if unlikely) for all of the philosophers to pick up their left forks simultaneously, and then block waiting for the next philospher over to put down their right fork. This situation is called deadlock, and will be studied to death in the next chapter.
Let's propose another solution to the problem. For this solution, we'll introduce an extra semaphore, which we'll call the Big Lock. Now the code looks like this:
void philospher(int me)
{
while (1) {
philosphize();
down(biglock);
down(fork[me]);
down(fork[(me+1) % 5];
eat();
up(fork[me]);
up(fork[(me+1) % 5];
up(biglock);
}
}
This solution can't deadlock - but it also isn't very good. Only one philosopher can be eating at a time, even though two could actually have the needed resources at once.
Things get a lot messier if we try to maximize parallelism. What
we'll do this time is have every philosopher announce when they're
hungry, and be polite to one another. The s semaphores
are initialized to 0, so the first time you try to do a
down() on it you block. Notice that we aren't
representing each resource by a semaphore any more: as a matter of
fact, we aren't explicitly representing the resources at all!
void philospher(int me)
{
while (1) {
think();
take_forks(me);
eat();
put_forks(me);
}
}
void take_forks(int me)
{
down(littlelock);
state[me] = HUNGRY;
test(me);
up(littlelock);
down(s[me]);
}
void put_forks(int me)
{
down(littlelock);
state[me] = THINKING;
test((me + 4) % 5);
test((me + 1) % 5);
up(littlelock);
}
void test(int phil)
{
if ((state[phil] == HUNGRY) &&
(state[(phil + 4) % 5] != EATING) &&
(state[(phil + 1) % 5] != EATING)) {
state[phil] = EATING;
up(s[phil]);
}
}