Thread Communication Using Wait( ), Pulse( ), and PulseAll( )


Consider the following situation. A thread called T is executing inside a lock block and needs access to a resource, called R, that is temporarily unavailable. What should T do? If T enters some form of polling loop that waits for R, then T ties up the object, blocking other threads’ access to it. This is a less than optimal solution because it partially defeats the advantages of programming for a multithreaded environment. A better solution is to have T temporarily relinquish control of the object, allowing another thread to run. When R becomes available, T can be notified and resume execution. Such an approach relies upon some form of interthread communication in which one thread can notify another that it is blocked, and be notified when it can resume execution. C# supports interthread communication with the Wait( ), Pulse( ), and PulseAll( ) methods.

The Wait( ), Pulse( ), and PulseAll( ) methods are defined by the Monitor class. These methods can be called only from within a locked block of code. Here is how they are used. When a thread is temporarily blocked from running, it calls Wait( ). This causes the thread to go to sleep and the lock for that object to be released, allowing another thread to use the object. At a later point, the sleeping thread is awakened when some other thread calls Pulse( ) or PulseAll( ). A call to Pulse( ) resumes the first thread in the queue of threads waiting for the lock. A call to PulseAll( ) signals the release of the lock to all waiting threads.

Here are two commonly used forms of Wait( ):

 public static bool Wait(object waitOb) public static bool Wait(object waitOb, int milliseconds)

The first form waits until notified. The second form waits until notified or until the specified period of milliseconds has expired. For both, waitOb specifies the object upon which to wait.

Here are the general forms for Pulse( ) and PulseAll( ):

 public static void Pulse(object waitOb) public static void PulseAll(object waitOb)

Here, waitOb is the object being released.

A SynchronizationLockException will be thrown if Wait( ), Pulse( ), or PulseAll( ) is called from code that is not within a lock block.

An Example That Uses Wait( ) and Pulse( )

To understand the need for and the application of Wait( ) and Pulse( ), we will create a program that simulates the ticking of a clock by displaying the words “Tick” and “Tock” on the screen. To accomplish this, we will create a class called TickTock that contains two methods: tick( ) and tock( ). The tick( ) method displays the word “Tick” and tock( ) displays “Tock”. To run the clock, two threads are created, one that calls tick( ) and one that calls tock( ). The goal is to make the two threads execute in a way that the output from the program displays a consistent “Tick Tock”—that is, a repeated pattern of one “Tick” followed by one “Tock”.

 // Use Wait() and Pulse() to create a ticking clock. using System; using System.Threading; class TickTock {   public void tick(bool running) {     lock(this) {       if(!running) { // stop the clock         Monitor.Pulse(this); // notify any waiting threads         return;       }       Console.Write("Tick ");       Monitor.Pulse(this); // let tock() run       Monitor.Wait(this); // wait for tock() to complete     }   }   public void tock(bool running) {     lock(this) {       if(!running) { // stop the clock         Monitor.Pulse(this); // notify any waiting threads         return;       }       Console.WriteLine("Tock");       Monitor.Pulse(this); // let tick() run       Monitor.Wait(this); // wait for tick() to complete     }   } } class MyThread {   public Thread thrd;   TickTock ttOb;   // Construct a new thread.   public MyThread(string name, TickTock tt) {     thrd = new Thread(this.run);     ttOb = tt;     thrd.Name = name;     thrd.Start();   }   // Begin execution of new thread.   void run() {     if(thrd.Name == "Tick") {       for(int i=0; i<5; i++) ttOb.tick(true);       ttOb.tick(false);     }     else {       for(int i=0; i<5; i++) ttOb.tock(true);       ttOb.tock(false);     }   } } class TickingClock {   public static void Main() {     TickTock tt = new TickTock();     MyThread mt1 = new MyThread("Tick", tt);     MyThread mt2 = new MyThread("Tock", tt);     mt1.thrd.Join();     mt2.thrd.Join();     Console.WriteLine("Clock Stopped");   } }

Here is the output produced by the program:

 Tick Tock Tick Tock Tick Tock Tick Tock Tick Tock Clock Stopped

Let’s take a close look at this program. In Main( ), a TickTock object called tt is created, and this object is used to start two threads of execution. Inside the run( ) method of MyThread, if the name of the thread is “Tick”, calls to tick( ) are made. If the name of the thread is “Tock”, the tock( ) method is called. Five calls that pass true as an argument are made to each method. The clock runs as long as true is passed. A final call that passes false to each method stops the clock.

The most important part of the program is found in the tick( ) and tock( ) methods. We will begin with the tick( ) method, which, for convenience, is shown here:

 public void tick(bool running) {   lock(this) {     if(!running) { // stop the clock       Monitor.Pulse(this); // notify any waiting threads       return;     }     Console.Write("Tick ");     Monitor.Pulse(this); // let tock() run     Monitor.Wait(this); // wait for tock() to complete   } }

First, notice that the code in tick( ) is contained within a lock block. Recall, Wait( ) and Pulse( ) can be used only inside synchronized blocks. The method begins by checking the value of the running parameter. This parameter is used to provide a clean shutdown of the clock. If it is false, then the clock has been stopped. If this is the case, a call to Pulse( ) is made to enable any waiting thread to run. We will return to this point in a moment. Assuming that the clock is running when tick( ) executes, the word “Tick” is displayed, and then a call to Pulse( ) takes place followed by a call to Wait( ). The call to Pulse( ) allows a thread waiting on the same object to run. The call to Wait( ) causes tick( ) to suspend until another thread calls Pulse( ). Thus, when tick( ) is called, it displays one “Tick”, lets another thread run, and then suspends.

The tock( ) method is an exact copy of tick( ), except that it displays “Tock”. Thus, when entered, it displays “Tock”, calls Pulse( ), and then waits. When viewed as a pair, a call to tick( ) can be followed only by a call to tock( ), which can be followed only by a call to tick( ), and so on. Therefore, the two methods are mutually synchronized.

The reason for the call to Pulse( ) when the clock is stopped is to allow a final call to Wait( ) to succeed. Remember, both tick( ) and tock( ) execute a call to Wait( ) after displaying their message. The problem is that when the clock is stopped, one of the methods will still be waiting. Thus, a final call to Pulse( ) is required in order for the waiting method to run. As an experiment, try removing this call to Pulse( ) and watch what happens. As you will see, the program will “hang” and you will need to press CTRL-C to exit. The reason for this is that when the final call to tock( ) calls Wait( ), there is no corresponding call to Pulse( ) that lets tock( ) conclude. Thus, tock( ) just sits there, waiting forever.

Before moving on, if you have any doubt that the calls to Wait( ) and Pulse( ) are actually needed to make the “clock” run right, substitute this version of TickTock into the preceding program. It has all calls to Wait( ) and Pulse( ) removed.

 // A non-functional version of TickTock. class TickTock {   public void tick(bool running) {     lock(this) {       if(!running) { // stop the clock         return;       }       Console.Write("Tick ");     }   }   public void tock(bool running) {     lock(this) {       if(!running) { // stop the clock         return;       }       Console.WriteLine("Tock");     }   } }

After the substitution, the output produced by the program will look like this:

 Tick Tick Tick Tick Tick Tock Tock Tock Tock Tock Clock Stopped

Clearly, the tick( ) and tock( ) methods are no longer synchronized!

Deadlock

When developing multithreaded programs, you must be careful to avoid deadlock. Deadlock is, as the name implies, a situation in which one thread is waiting for another thread to do something, but that other thread is waiting on the first. Thus, both threads are suspended, waiting for each other, and neither executes. This situation is analogous to two overly polite people both insisting that the other step through a door first!

Avoiding deadlock seems easy, but it’s not. For example, deadlock can occur in roundabout ways. Consider the TickTock class. As explained, if a final Pulse( ) is not executed by tick( ) or tock( ), then one or the other will be waiting indefinitely and the program is deadlocked. Often the cause of the deadlock is not readily understood just by looking at the source code to the program, because concurrently executing threads can interact in complex ways at runtime. To avoid deadlock, careful programming and thorough testing are required. In general, if a multithreaded program occasionally “hangs,” deadlock is the likely cause.




C# 2.0(c) The Complete Reference
C# 2.0: The Complete Reference (Complete Reference Series)
ISBN: 0072262095
EAN: 2147483647
Year: 2006
Pages: 300

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net