cs205: engineering software? |
(none) 05 April 2008 |
Problem Set 5 Distributed Simulations (Object-Oriented Design and Concurrency) |
Out: 11 October Design Reviews: (in class) Monday, 16 October Due: Wednesday, 25 October (beginning of class) |
Collaboration Policy (read carefully)
For questions 1-6 of this problem set, you should work alone and turn in your own solution. You may discuss your work with other students in the class, but the work you turn in should be your own.Design ReviewsFor questions 7-9 you are expected to work in a team of three students. If you would like to form your own team, send me email before 11:59pm on Thursday, October 12, saying who your teammates are. If I don't receive a team request from you before then, or I receive confusing or inconsistent requests, you will be assigned to a team.
In class on Monday, 16 October, each team will be expected to present their design (question 7). This design should explain: (1) what it is you are simulating; (2) the key modules you will need to implement and their dependencies and inheritance relationships; (3) your implementation strategy including how you will divide the work amongst your team and how you will implement and test your modules. For the design review, you should have a short paper document that answers these questions and be able to present and answer questions about your design.
Reading: Chapter 14. |
Purpose
Objects in the simulator are subtypes of the SimObject type, partially specified below:
abstract public class SimObject implements Runnable { // OVERVIEW: A SimObject is an object than represents a simulator // object. It has a grid (a circular reference to the grid // containing this object), location (integer row and column), // initialization state (boolean that is true if it has beein // initialized), and thread pause state (boolean that is true // if the thread is paused). // A typical SimObject is // < grid, row, col, initialized, paused >. public SimObject() // EFFECTS: Creates a new, uninitalized SimObject. abstract public void executeTurn() // REQUIRES: this is initialized // MODIFIES: this // EFFECTS: Executes one turn for this object. public Color getColor() // EFFECTS: Returns the color that represents this SimObject, // for painting the grid. final public int getX() // REQUIRES: this is initialized. // EFFECTS: Returns the x location that this cell is located in. final public int getY() // REQUIRES: init has previously been called for this. // EFFECTS: Returns the y location that this cell is located in. final public void init(int x, int y, Grid grid) throws BadLocationException // REQUIRES: this is uninitialized // MODIFIES: this // EFFECTS: If x, y is not a valid location on grid, throws // BadLocationException. Otherwise, initializes the cell at // row, col on grid with isPaused = true. public void run() // REQUIRES: this is initialized // MODIFIES: this // EFFECTS: Implements the thread for this object. // Note: This method can do anything. // For the base class, if the object is not paused, // it executes one turn by calling the executeTurn method, // and sleeps for a time and repeats. If the object is // paused, does nothing (until the object is unpaused).Note that SimObject is an abstract class — we cannot instantiate objects of type SimObject. The abstract method executeTurn must be implemented by subtypes.
SimObject implements the Runnable interface (that is, it is a subtype of Runnable) which requires it to implement the run method. A Runnable object can be used to create a new thread using the Thread (Runnable) constructor.
For example, here is the code in the init method of SimObject.java that starts a thread for each simulated object:
Thread thread = new Thread(this); thread.start();The this object passed to Thread is a SimObject (or a subtype of SimObject). When the thread starts, it will call the run method of this object.
The Java API documentation specification of Runnable.run method is (what folows is the actualy Java Platform API documentation):
void run() When an object implementing interface Runnable is used to create a thread, starting the thread causes the object's run method to be called in that separately executing thread. The general contract of the method run is that it may take any action whatsoever.
The important classes in the walker simulator are: Grid, GridDisplay, Coordinate, Direction, MobileSimObject, RandomWalker, SimObject, Simulator, WalkerSimulator.
If you start with enough objects (or are lucky), you will get some RuntimeExceptions in the Console like this:
java.lang.RuntimeException: BUG: SimObject.setLocation - row: 22 col: 27 already occupied. at ps5.MobileSimObject.setLocation(MobileSimObject.java:15) at ps5.RandomWalker.executeTurn(RandomWalker.java:39) at ps5.SimObject.run(SimObject.java:172) at java.lang.Thread.run(Thread.java:536)Our grid allows only one object in each square, but we are getting exceptions in setLocation when a MobileSimObject attempts to wander into a cell that is already occupied. Each unhandled exception terminates the tread raising the exception, but does not shut down the application. The other threads keep executing normally.
What's going on here? A first guess might be that the RandomWalker method for executeTurn does not check if the new square is empty before moving into it.
Looking at the code, however, we see that that is not the case:
public void executeTurn() throws RuntimeException // EFFECTS: Picks a random direction and tries to move that way. // If the move is successful, return true. If the move fails // because the spot being attempted to move into is already // occupied then return false. { Direction dir = Direction.randomDirection(); int newy = getY() + dir.northerlyDirection(); int newx = getX() + dir.easterlyDirection(); if (getGrid().validLocation(newx, newy)) { if (getGrid().isSquareEmpty(newx, newy)) { setLocation(newx, newy); } } }(Note: the actual code is slightly different from this to increase the chances of observing the race condition. There is a delay before the call to setLocation. The exception handler in the actual code is also omitted here.)
The code calls getGrid().isSquareEmpty (newx, newy) to check if the square is empty, and then setLocation (newx, newy) to move into the new square. But, what happens if another object moves into that square in the time between this object's isSquareEmpty(newx, newy) test and the call to setLocation(newx, newy)?
This is an example of a race condition — two threads are reading and writing to the same data (in this case, the Grid object square). Depending on the order in which the threads execute, the result may be different.
The most common way to prevent race conditions is to use locks to prevent two threads from executing critical regions of code at the same time. We would like to know that between the time this object's thread calls isSquareEmpty and the completion of the setLocation call, no other thread can alter the state of the grid. In Java, we do this using a synchronized statement:
synchronized (expr) { statements }The expr must evaluate to an object reference. There is a lock associated with every object. When a synchronized statement is reached, the executing thread will evaluate expr and attempt to acquire the lock for the object it evaluates to. If the lock is available (that is, no other thread has acquired it), this thread will acquire the lock and hold it until the end of the synchronized block. Hence, any other thread that synchronizes on the same object will stall until this thread releases the lock.
Using synchronized in the method header is a short cut for synchronizing on this. So,
synchronized public int f () { ... }means the same thing as:
public int f () { synchronized (this) { ... } }
In our experience, we find philosophers perfer not to share chopsticks, but they do like to philosophize and argue. Consider the Philosopher class:
// Loosely based on Arnold, Gosling, Holmes p. 252 class PhilosopherThread implements Runnable { private Philosopher philosopher; // Rep Invariant: philosopher != null public PhilosopherThread(Philosopher p) { philosopher = p; } public void run() { philosopher.philosophize(); } } public class Philosopher { private Philosopher colleague; private String name; private String quote; // Rep Invariant: name != null, quote != null (colleague may be null) public Philosopher(String name, String quote) { this.name = name; this.quote = quote; } public synchronized void setColleague(Philosopher p) { colleague = p; } public synchronized void philosophize() { System.err.println(name + "[Thread " + Thread.currentThread().getName() + "] says " + quote); if (colleague != null) { // Need a colleague to start an argument. colleague.argue(); } } public synchronized void argue() // REQUIRES: this.colleague != null { System.err.println(name + "[Thread " + Thread.currentThread().getName() + "] argues: No! " + quote); } public static void main(String[] args) { Philosopher descartes = new Philosopher("Descartes", "I think, therefore I am."); Philosopher plato = new Philosopher( "Plato", "The life which is unexamined is not worth living."); descartes.setColleague(plato); plato.setColleague(descartes); Thread dthread = new Thread(new PhilosopherThread(descartes)); Thread pthread = new Thread(new PhilosopherThread(plato)); dthread.start(); pthread.start(); } }What happens when both philosophers start philsophizing at the same time?
Suppose the descartes thread runs first an invokes the philosophize method. Since it is declared with synchronized, before the method begins executing it must acquire the lock on its this object (in this case, that is the Philosopher object descartes). Then it calls the colleague.argue method. The argue method is also declared to be synchronized, so it must acquire a lock on its this object before proceeding. Here, argue is invoked on the object referenced by colleague (which is the plato object in the example). After the argue method finished, it releases the lock on plato. Then, the caller, and philosophize releases its lock on descartes.
But, what would happen if the threads executed in a different order? Suppose the descartes thread runs first and invokes the philosophize method, and then the plato thread invokes its philosophize method before the descartes thread calls colleague.argue. This is called a deadlock.
You may want to try running the code to see what is happening more closely. To increase the chances of seeing the deadlock, insert a delay before the call to colleague.argue:
try { Thread.sleep (500); // Pause for 500 ms. } catch (InterruptedException ie) { ; }
Producing multithreaded code that does not have races or deadlocks is very tricky. It is especially difficult since even if the program is tested thoroughly, the problem may appear and disapper based on what other processes running on the machine are doing.
public void philosophize () { Object lock1, lock2; if (colleague != null) { // Need a colleague to start and argument. // Always grab the lock for whichever name is alphabetically first if (name.compareTo (colleague.name) < 0) { lock1 = this; lock2 = colleague; } else { lock1 = colleague; lock2 = this; } synchronized (lock1) { synchronized (lock2) { System.err.println (name + "[Thread " + Thread.currentThread().getName () + "] says " + quote); colleague.argue (); } } } }
The provided code includes a DrunkPhilosopher class that is a subtype of the RandomWalker type from question 5. A DrunkPhilosopher object wanders around until she finds a colleague, and then starts philosophizing. Our DrunkPhilosopher suffers from a similar deadlock problem to the Philosopher code above. Try running java PhilosopherSimulator and observe what happend when two DrunkPhilosopher objects encounter each other and deadlock.
Be creative! A good simulation will demonstrate some interesting phenomenon or help answer an important social or scientific question. You may with alone, or with any number of students of your choosing (if you can convince people not in cs205 to also contribute to your program, that is fine also, so long as you clearly document what you did).
Listed below are a few ideas for things to simulate, but you are encouraged to think of your own. Since you are working in teams of three, you should be able to implement a simulation with several different types of objects.
Note: the source code for these examples is available (usually linked from the example page). If you want to incorporate code from these in your simulation, you may, but you must acknowledge your sources and explain clearly the changes you made. Obviously, making a small change to a previous answer would not be considered a satisfactory answer for this assignment.
Have fun playing the games...but don't get too distracted from doing your own work!
Each team should bring their answer to question 7 to class on Monday, 16 October, prepared to present it and answer questions about it.
If you look at the API spec for java.applet.Applet, you will see that it inherits methods from Panel (1 method), Container (47 methods), Component (56 methods) and Object (10 methods), as well as defining 25 new methods itself. So, there are 139 Applet methods! Fortunately, you can make useful applets only using a few of them directly.
In your Applet subtype, you will define replacement methods for some of the Applet methods:
where ps5.PhilosopherApplet.class is a class that is a subtype of Applet and the classes are found in the ps5/ subdirectory from the web page location.<html> <body> <applet code="ps5.PhilosopherApplet.class" width="520" height="590"></applet> </body> </html>
(We won't cover any more HTML in this class. If you want to incorporate your applets into fancier web pages, a guide to HTML is available here: http://www.cs.virginia.edu/cs150/guides/html-guide.html.)
Turn-in Checklist: You should turn in your answers to all questions on paper at the beginning of class on Wednesday, 25 October. including your code (but not unnecessary printouts of the provided code). Your answers to questions 1-6 should be turned in individually. Each team should turn in a single document that contains your answers to questions 7-9. Also, submit a zip file containing (1) all of your code and (2) the URL for your applet page, by email to evans@cs.virginia.edu.