CS 10: Winter 2016

Lecture 28, March 7

Code discussed in lecture

Dining Philosophers

Allowing only one thread in any synchronized method at a given time allows us to deal with the concurrency problems we saw with Incrementer, but it can be overly restrictive in some cases. For instance, allowing only one thread to access a large database would be overly restrictive. It would be better to allow a program to "lock" a given piece of data while it processed it, while allowing other programs to process other pieces of data in the same database. It is often useful to have resources that can be "checked out" for your exclusive use and then "checked back in" when you are done. This can be accomplished via a mechanism called a semaphore in some of the literature and a lock in other parts of the literature. The basic idea is that you can ask to acquire a resource, and only one request will be granted at a time and the resource will be unavailable to other threads or processes until it is released.

While this allows more flexible access to resources and more concurrent processing, it leads to two nasty problems: deadlock and starvation. Deadlock occurs when the pattern of resource acquisition has produced a state where no thread in a set of threads may proceed because each needs a resource that another is holding. They wait forever. Starvation is when some thread never gets a resource that it needs, even though the needed resources are acquired and freed by other processes. The particular process just never manages to get all of the resources it needs at the same time.

These two problems are commonly illustrated in the "dining philosophers" problem. Suppose there are n philosophers sitting around a circular table with n forks, one between each pair of philosophers. Every so often, a philosopher decides to eat, and to do so must pick up two forks — one on his left and one on his right. After acquiring the forks, the philosopher eats for a while before putting them both back down. Deadlock and starvation can arise because of the limited resources (for some reason, those poor philosophers never have enough forks to go around).

In the naive fork acquisition algorithm, a philosopher first picks up the left fork and then the right fork. So it can be the case that each gets the left fork, but the right fork is held by their neighbor. We have a deadlock, where nobody can proceed.

For a deadlock to occur, we need three conditions:

  1. Each process or thread needs multiple resources.
  2. A thread can hold some resources while waiting to acquire others
  3. A circular "wait-for" pattern can occur

The algorithm described certainly satisfies all of these conditions. Each philosopher needs 2 forks. As described above he picks up one and then tries to pick up the other. And we can have each philosopher holding his left fork while waiting for his right fork, so we can get a circular wait-for pattern.

So how can we solve this problem? One easy way it to make 2 impossible. We can allow a philosopher to pick up forks only if both are available. But how can we do this? What if both forks are free when the philosopher checks, but a neighbor grabs the second one after he picks up the first? A monitor will solve that problem. Consider the classes ForksMonitor and PhilosopherMonitor.

First, we create a class to deal with the forks. The forks are represented by a boolean array, where true indicates the fork is in use. Note that a philosopher trying to pick up forks can only do so if both are free. The synchronized qualifier prevents another philosopher from grabbing one of them after he checks. After eating a philosopher puts down both forks and notifies any waiting philosophers that they can try to proceed.

public class ForksMonitor { public boolean [] forks; // True indicates that the fork is taken /** * Construct a object with num forks. * Mark all as not in use. * @param num number of forks (and philosophers) */ public ForksMonitor(int num) { forks = new boolean[num]; for(int i = 0; i < forks.length; i++) forks[i] = false; } /** * Pick up forks numbered pNum and pNum+1 (wrapping). * Block if either fork is in use. * pNum the number of the first fork */ public synchronized void pickUpForks(int pNum) throws InterruptedException { // If either fork is in use wait while (forks[pNum] || forks[(pNum+1) % forks.length] ) wait(); // Take the forks forks[pNum] = true; forks[(pNum+1) % forks.length] = true; } /** * Put down forks numbered pNum and pNum+1 (wrapping). * Assumes that this philosopher had been granted the forks. * pNum the number of the first fork */ public synchronized void putDownForks(int pNum) throws InterruptedException { // Put down the forks forks[pNum] = false; forks[(pNum+1) % forks.length] = false; notifyAll(); } }

The philosophers each get a thread. In their eat method each picks up the forks (perhaps after waiting), eats, puts down the forks, and thinks. This is repeated NUM_MEALS times in each philosopher's run method. Demonstrate.

public class PhilosophersMonitor extends Thread { private int myNum; // Number of this philosopher private ForksMonitor forks; private Random generator; private static final int EAT_TIME = 1000; private static final int THINK_TIME = 1000; private static final int NUM_MEALS = 10; private static final int NUM_PHILOSOPHERS = 6; public PhilosophersMonitor(int philNum, ForksMonitor theForks) { myNum = philNum; forks = theForks; generator = new Random(); } /** * Eat using a monitor to control the forks * @param meal the number of the meal eaten */ public void eat(int meal) throws InterruptedException { System.out.println("Philosopher " + myNum + " tries to pick up forks"); forks.pickUpForks(myNum); System.out.println("Philosopher " + myNum + " picks up forks and eats meal " + meal); sleep(EAT_TIME + generator.nextInt(EAT_TIME)); System.out.println("Philosopher " + myNum + " puts down forks and thinks"); forks.putDownForks(myNum); sleep(THINK_TIME + generator.nextInt(THINK_TIME)); } /** * The code to eat NUM_MEALS meals is in the run method of the thread. */ public void run() { try { for(int meal = 1; meal <= NUM_MEALS; meal++) eat(meal); } catch(InterruptedException e){} } /** * Create NUM_PHILOSOPHERS philosopher threads and start them eating */ public static void main(String [] args) { ForksMonitor forks = new ForksMonitor(NUM_PHILOSOPHERS); for(int num = 0; num < NUM_PHILOSOPHERS; num++) { PhilosophersMonitor phil = new PhilosophersMonitor(num, forks); phil.start(); } } }

This approach does have drawbacks. A philosopher can starve if one of his neigbors always has a fork. As long as one of them is eating while the other is thinking this can occur. Using a monitor also prevents two philosophers on opposite sides of the table from picking up forks simultaneously, which is not necessary.

An alternate approach is to protect each fork. There is a construct in concurrent programming called a semaphore, and it is implemented in the Java library. A semaphore allows access to a pool of identical resources. It is basically a synchronized counter that keeps track of how many of the resources are currently available. If s is a semaphore, then calling s.acquire() grants a "permit" to one of the resources and reduces the count of available resources. Calling s.release() returns the permit to the pool, increasing the count of available resources and allowing another process to acquire the resource that has been released. If the pool is empty when s.acquire() is called the process blocks and waits for a resource to be released.

When constructing a Semaphore object you give it two parameters: the number of objects in the pool and a boolean, where true means that the queue waiting for a resource to be released should be fair (FIFO) and false means that the waiting queue need not be FIFO.

The class PhilosopherSemaphore creates an array of Semaphore objects, one for each fork (so the initial size of each pool is 1). It creates the appropriate number of philosophers (each with their own number), and and calls start on each philosopher to start it running.

PhilosoperSemaphore extends Thread. Each philosopher remembers his own number. When it is time to eat he first acquires the left fork and then the right. We call the method that does this starve, because it can deadlock. The run method calls starve repeatedly, with pauses for eating before putting down the forks and pauses for thinking before trying to eat again. Demonstrate. The sleep(10) before picking up the second fork delays it enough to cause a deadlock.

public class PhilosopherSemaphore extends Thread { private int myNum; // Number of this philosopher private static Semaphore [] forks; private Random generator; private static final int EAT_TIME = 1000; private static final int THINK_TIME = 1000; private static final int NUM_MEALS = 10; private static final int NUM_PHILOSOPHERS = 6; public PhilosopherSemaphore(int philNum) { myNum = philNum; generator = new Random(); } /** * Eat using semaphores to control the forks This version can (and probably * will) deadlock */ public void starve(int meal) throws InterruptedException { System.out.println("Philosopher " + myNum + " tries to pick up fork " + myNum); forks[myNum].acquire(); System.out.println("Philosopher " + myNum + " has fork " + myNum + " and tries to pick up fork " + ((myNum + 1) % NUM_PHILOSOPHERS)); sleep(10); forks[(myNum + 1) % NUM_PHILOSOPHERS].acquire(); System.out.println("Philosopher " + myNum + " eats meal " + meal); sleep(EAT_TIME + generator.nextInt(EAT_TIME)); System.out.println("Philosopher " + myNum + " puts down forks and thinks"); forks[myNum].release();; forks[(myNum + 1) % NUM_PHILOSOPHERS].release(); sleep(THINK_TIME + generator.nextInt(THINK_TIME)); } public void run() { try { for (int meal = 1; meal <= NUM_MEALS; meal++) starve(meal); } catch (InterruptedException e) { } } public static void main(String[] args) { forks = new Semaphore[NUM_PHILOSOPHERS]; for(int i = 0; i < NUM_PHILOSOPHERS; i++) forks[i] = new Semaphore(1, true); for (int num = 0; num < NUM_PHILOSOPHERS; num++) { PhilosopherSemaphore phil = new PhilosopherSemaphore(num); phil.start(); } }

How can we avoid this deadlock? Make it so that condition 3 (circular wait) cannot happen. One approach is for each philosopher to grab the lower-numbered fork first. This is a special case of a general approach to avoiding deadlocks. This approach puts a linear order on the resources, so each resource R has a unique priority number p(R). We require a task to acquire the resources in increasing order of p(R) and release them in decreasing order. If all resources are released at the same time the order that they are released in does not matter, but if you are going to release some - but not all - resources and then acquire others it does matter. If you want to free a resource R you must first free all resources R' with p(R') > p(R), even if you will later re-acquire one or more of these resources after acquiring R.

Why does this prevent a loop? If there were a loop one of the threads T in the loop would hold a resource R whose p(R) is greater than the p value all of the resources held by the other threads in the loop. T must be waiting to acquire a resource, because otherwise it would proceed. But the rules above say that the resource R' that T is waiting to acquire must have p(R') > p(R), because threads acquire resources in increasing order of p. But this means that no other thread can be holding R', because R had the largest p value of all the resources held by all threads in the loop. Thus T cannot be waiting for a resource held by another thread in the loop, so no loop can exist.

Another approach is to have a waiter prevent the last philosopher from taking the last fork until some other philospher puts down his forks.

Perhaps the most elegant approach is to have even-numbered philosophers grab their left fork first and odd-numbered philosophers grab their right fork first. (For an even number of philosophers this effectively breaks the forks into two resource classes, those to be acquired first and those to be acquired second. For an odd number it is a bit more complicated, but still works.) This is approach is seen in the eat method of PhilosopherSemaphore. Demonstrate it.

/** * Eat using semaphores to control the forks This version cannot deadlock */ public void eat(int meal) throws InterruptedException { if (myNum % 2 == 0) { System.out.println("Philosopher " + myNum + " tries to pick up fork " + myNum); forks[myNum].acquire();; System.out.println("Philosopher " + myNum + " has fork " + myNum + " and tries to pick up fork " + ((myNum + 1) % NUM_PHILOSOPHERS)); sleep(10); forks[(myNum + 1) % NUM_PHILOSOPHERS].acquire();; System.out.println("Philosopher " + myNum + " eats meal " + meal); } else { System.out.println("Philosopher " + myNum + " tries to pick up fork " + ((myNum + 1) % NUM_PHILOSOPHERS)); forks[((myNum + 1) % NUM_PHILOSOPHERS)].acquire(); System.out.println("Philosopher " + myNum + " has fork " + ((myNum + 1) % NUM_PHILOSOPHERS) + " and tries to pick up fork " + myNum); sleep(10); forks[myNum].acquire();; System.out.println("Philosopher " + myNum + " eats meal " + meal); } sleep(EAT_TIME + generator.nextInt(EAT_TIME)); System.out.println("Philosopher " + myNum + " puts down forks and thinks"); forks[myNum].release();; forks[(myNum + 1) % NUM_PHILOSOPHERS].release();; sleep(THINK_TIME + generator.nextInt(THINK_TIME)); }

Other Approaches

There are other approaches for handling concurrent processes and threads. One is message passing. Instead of sharing memory processes send messages to one another.

Another is called Software Transactional Memory. The idea is that you specify a group of actions that should take place atomically. That is, nothing else should access or change the information that the actions are reading or changing. The idea comes from databases. When you are dealing with records in a database it is impractical to lock the entire database to do a transaction. The way that databases work is that you perform a group of operations called a transaction. All actions are recorded in a log. You then commit the transaction. If no other process has interacted with the records that the transaction has dealt with (which can be determined from the log) you record all changes. Otherwise you "roll back" to the state before you began the transaction. Thus the whole transaction completes successfully or nothing appears to have happened at all.

This just scratches the surface of the topic of concurrent programs. One thing that we have not discussed is how to actually implement synchronized regions or semaphores. Without hardware support it is quite tricky. The thing that you typically do to enter a synchronized region is to test a boolean variable (e.g. isInUse) to see if another thread is in the region, and if not set the variable to true and proceed into the region. Unfortunately, between the time you fetch the variable and the time that you set it to true other threads may have also fetched the variable and assumed that they can go into the synchronized region. Therefore the test has to be done in stages, where instead of entering the synchronized region immediately you first have to enter code that will guarantee that even when several threads get past the boolean test that only one will be able to enter the synchronized region. This code is quite tricky and it is very easy to get it wrong. (We don't do a good job of imagining all ways that threads can interleave.) I first saw this in an Operating Systems course.

For that reason many computers have some sort of "test and set" instruction that atomically fetches the value of the variable and sets it to a value. That way if the variable is false when you test-and-set it you may safely proceed. No thread can grab the value between the time that you fetch it and the time that you set it to true.