Thread in C# - Part 17: Wait and Pulse (1)  
 

(Post 04/12/2007) Earlier we discussed Event Wait Handles – a simple signaling mechanism where a thread blocks until it receives notification from another.

A more powerful signaling construct is provided by the Monitor class, via two static methods – Wait and Pulse. The principle is that you write the signaling logic yourself using custom flags and fields (in conjunction with lock statements), then introduce Wait and Pulse commands to mitigate CPU spinning. This advantage of this low-level approach is that with just Wait, Pulse and the lock statement, you can achieve the functionality of AutoResetEvent, ManualResetEvent and Semaphore, as well as WaitHandle's static methods WaitAll and WaitAny. Furthermore, Wait and Pulse can be amenable in situations where all of the Wait Handles are parsimoniously challenged.

A problem with Wait and Pulse is their poor documentation – particularly with regard their reason-to-be. And to make matters worse, the Wait and Pulse methods have a peculiar aversion to dabblers: if you call on them without a full understanding, they will know – and will delight in seeking you out and tormenting you! Fortunately, there is a simple pattern one can follow that provides a fail-safe solution in every case.

Wait and Pulse Defined

The purpose of Wait and Pulse is to provide a simple signaling mechanism: Wait blocks until it receives notification from another thread; Pulse provides that notification.

Wait must execute before Pulse in order for the signal to work. If Pulse executes first, its pulse is lost, and the late waiter must wait for a fresh pulse, or remain forever blocked. This differs from the behavior of an AutoResetEvent, where its Set method has a "latching" effect and so is effective if called before WaitOne.

One must specify a synchronizing object when calling Wait or Pulse. If two threads use the same object, then they are able to signal each other. The synchronizing object must be locked prior to calling Wait or Pulse.

For example, if x has this declaration:

class Test {
// Any reference-type object will work as a synchronizing object
object x = new object();
}

then the following code blocks upon entering Monitor.Wait:

lock (x) Monitor.Wait (x);

The following code (if executed later on another thread) releases the blocked thread:

lock (x) Monitor.Pulse (x);

Lock toggling

To make this work, Monitor.Wait temporarily releases, or toggles the underlying lock while waiting, so another thread (such as the one performing the Pulse) can obtain it. The Wait method can be thought of as expanding into the following pseudo-code:

Monitor.Exit (x); // Release the lock
wait for a pulse on x
Monitor.Enter (x); // Regain the lock

Hence a Wait can block twice: once in waiting for a pulse, and again in regaining the exclusive lock. This also means that Pulse by itself does not fully unblock a waiter: only when the pulsing thread exits its lock statement can the waiter actually proceed.

Wait's lock toggling is effective regardless of the lock nesting level. If Wait is called inside two nested lock statements:

lock (x)
lock (x)
Monitor.Wait (x);

then Wait logically expands into the following:

Monitor.Exit (x); Monitor.Exit (x); // Exit twice to release the lock
wait for a pulse on x
Monitor.Enter (x); Monitor.Enter (x); // Restore previous nesting level

Consistent with normal locking semantics, only the first call to Monitor.Enter affords a blocking opportunity.

Why the lock?

Why have Wait and Pulse been designed such that they will only work within a lock? The primary reason is so that Wait can be called conditionally – without compromising thread-safety. To take a simple example, suppose we want to Wait only if a boolean field called available is false. The following code is thread-safe:

lock (x) {
if (!available) Monitor.Wait (x);
available = false;
}

Several threads could run this concurrently, and none could preempt another in between checking the available field and calling Monitor.Wait. The two statements are effectively atomic. A corresponding notifier would be similarly thread-safe:

lock (x)
if (!available) {
available = true;
Monitor.Pulse (x);
}

Specifying a timeout

A timeout can be specified when calling Wait, either in milliseconds or as a TimeSpan. Wait then returns false if it gave up because of a timeout. The timeout applies only to the "waiting" phase (waiting for a pulse): a timed out Wait will still subsequently block in order to re-acquire the lock, no matter how long it takes. Here's an example:

lock (x) {
if (!Monitor.Wait (x, TimeSpan.FromSeconds (10)))
Console.WriteLine ("Couldn't wait!");
Console.WriteLine ("But hey, I still have the lock on x!");
}

This rationale for this behavior is that in a well-designed Wait/Pulse application, the object on which one calls Wait and Pulse is locked just briefly. So re-acquiring the lock should be a near-instant operation.

Pulsing and acknowledgement

An important feature of Monitor.Pulse is that it executes asynchronously, meaning that it doesn't itself block or pause in any way. If another thread is waiting on the pulsed object, it's notified, otherwise the pulse has no effect and is silently ignored.

Pulse provides one-way communication: a pulsing thread signals a waiting thread. There is no intrinsic acknowledgment mechanism: Pulse does not return a value indicating whether or not its pulse was received. Furthermore, when a notifier pulses and releases its lock, there's no guarantee that an eligible waiter will kick into life immediately. There can be an arbitrary delay, at the discretion of the thread scheduler – during which time neither thread has the lock. This makes it difficult to know when a waiter has actually resumed, unless the waiter specifically acknowledges, for instance via a custom flag.

If reliable acknowledgement is required, it must be explicitly coded, usually via a flag in conjunction with another, reciprocal, Pulse and Wait.

Relying on timely action from a waiter with no custom acknowledgement mechanism counts as "messing" with Pulse and Wait. You'll lose!

Waiting queues and PulseAll

More than one thread can simultaneously Wait upon the same object – in which case a "waiting queue" forms behind the synchronizing object (this is distinct from the "ready queue" used for granting access to a lock). Each Pulse then releases a single thread at the head of the waiting-queue, so it can enter the ready-queue and re-acquire the lock. Think of it like an automatic car park: you queue first at the pay station to validate your ticket (the waiting queue); you queue again at the barrier gate to be let out (the ready queue).

The order inherent in the queue structure, however, is often unimportant in Wait/Pulse applications, and in these cases it can be easier to imagine a "pool" of waiting threads. Each pulse, then, releases one waiting thread from the pool.

Monitor also provides a PulseAll method that releases the entire queue, or pool, of waiting threads in a one-fell swoop. The pulsed threads won't all start executing exactly at the same time, however, but rather in an orderly sequence, as each of their Wait statements tries to re-acquire the same lock. In effect, PulseAll moves threads from the waiting-queue to the ready-queue, so they can resume in an orderly fashion.

How to use Pulse and Wait

Here's how we start. Imagine there are two rules:

  • the only synchronization construct available is the lock statement, aka Monitor.Enter and Monitor.Exit
  • there are no restrictions on spinning the CPU!

With those rules in mind, let's take a simple example: a worker thread that pauses until it receives notification from the main thread:

class SimpleWaitPulse {
bool go;
object locker = new object();

void Work() {
Console.Write ("Waiting... ");
lock (locker) { // Let's spin!
while (!go) {
// Release the lock so other threads can change the go flag
Monitor.Exit (locker);
// Regain the lock so we can re-test go in the while loop
Monitor.Enter (locker);
}
}
Console.WriteLine ("Notified!");
}

void Notify()// called from another thread
{
lock (locker) {
Console.Write ("Notifying... ");
go = true;
}
}
}

Here's a main method to set things in motion:

static void Main() {
SimpleWaitPulse test = new SimpleWaitPulse();

// Run the Work method on its own thread
new Thread (test.Work).Start(); // "Waiting..."

// Pause for a second, then notify the worker via our main thread:
Thread.Sleep (1000);
test.Notify(); // "Notifying... Notified!"
}

The Work method is where we spin – extravagantly consuming CPU time by looping constantly until the go flag is true! In this loop we have to keep toggling the lock – releasing and re-acquiring it via Monitor's Exit and Enter methods – so that another thread running the Notify method can itself get the lock and modify the go flag. The shared go field must always be accessed from within a lock to avoid volatility issues (remember that all other synchronization constructs, such as the volatile keyword, are out of bounds in this stage of the design!)

The next step is to run this and test that it actually works. Here's the output from the test Main method:

 

Waiting... (pause) Notifying... Notified!


Now we can introduce Wait and Pulse. We do this by:

  • replacing lock toggling (Monitor.Exit followed by Monitor.Enter) with Monitor.Wait
  • inserting a call to Monitor.Pulse when a blocking condition is changed (i.e. the go flag is modified).

Here's the updated class, with the Console statements omitted for brevity:

class SimpleWaitPulse {
bool go;
object locker = new object();

void Work() {
lock (locker)
while (!go) Monitor.Wait (locker);
}

void Notify() {
lock (locker) {
go = true;
Monitor.Pulse (locker);
}
}
}

The class behaves as it did before, but with the spinning eliminated. The Wait command implicitly performs the code we removed – Monitor.Exit followed by Monitor.Exit, but with one extra step in the middle: while the lock is released, it waits for another thread to call Pulse. The Notifier method does just this, after setting the go flag true. The job is done.

Pulse and Wait Generalized

Let's now expand the pattern. In the previous example, our blocking condition involved just one boolean field – the go flag. We could, in another scenario, require an additional flag set by the waiting thread to signal that's it's ready or complete. If we extrapolate by supposing there could be any number of fields involved in any number of blocking conditions, the program can be generalized into the following pseudo-code (in its spinning form):

class X {
Blocking Fields: one or more objects involved in blocking condition(s), eg
bool go; bool ready; int semaphoreCount; Queue consumerQ...

object locker = new object(); // protects all the above fields!

... SomeMethod {
... whenever I want to BLOCK based on the blocking fields:
lock (locker)
while (! blocking fields to my liking ) {
// Give other threads a chance to change blocking fields!
Monitor.Exit (locker);
Monitor.Enter (locker);
}

... whenever I want to ALTER one or more of the blocking fields:
lock (locker) { alter blocking field(s) }
}
}

We then apply Pulse and Wait as we did before:

  • In the waiting loops, lock toggling is replaced with Monitor.Wait
  • Whenever a blocking condition is changed, Pulse is called before releasing the lock.

Here's the updated pseudo-code:

Wait/Pulse Boilerplate #1: Basic Wait/Pulse Usage

class X {
< Blocking Fields ... >
object locker = new object();

... SomeMethod {
...
... whenever I want to BLOCK based on the blocking fields:
lock (locker)
while (! blocking fields to my liking )
Monitor.Wait (locker);

... whenever I want to ALTER one or more of the blocking fields:
lock (locker) {
alter blocking field(s)
Monitor.Pulse (locker);
}
}
}

This provides a robust pattern for using Wait and Pulse. Here are the key features to this pattern:

  • Blocking conditions are implemented using custom fields (capable of functioning without Wait and Pulse, albeit with spinning)
  • Wait is always called within a while loop that checks its blocking condition (itself within a lock statement)
  • A single synchronization object (in the example above, locker) is used for all Waits and Pulses, and to protect access to all objects involved in all blocking conditions
  • Locks are held only briefly

Most importantly, with this pattern, pulsing does not force a waiter to continue. Rather, it notifies a waiter that something has changed, advising it to re-check its blocking condition. The waiter then determines whether or not it should proceed (via another iteration of its while loop) – and not the pulser. The benefit of this approach is that it allows for sophisticated blocking conditions, without sophisticated synchronization logic.

Another benefit of this pattern is immunity to the effects of a missed pulse. A missed pulse happens when Pulse is called before Wait – perhaps due to a race between the notifier and waiter. But because in this pattern a pulse means "re-check your blocking condition" (and not "continue"), an early pulse can safely be ignored since the blocking condition is always checked before calling Wait, thanks to the while statement.

With this design, one can define multiple blocking fields, and have them partake in multiple blocking conditions, and yet still use a single synchronization object throughout (in our example, locker). This is usually better than having separate synchronization objects on which to lock, Pulse and Wait, in that one avoids the possibility of deadlock. Furthermore, with a single locking object, all blocking fields are read and written to as a unit, avoiding subtle atomicity errors. It's a good idea, however, not to use the synchronization object for purposes outside of the necessary scope (this can be assisted by declaring private the synchronization object, as well as all blocking fields).

(Sưu tầm)


 
 

 
     
 
Công nghệ khác:


Thread in C# - Part 16: Non-Blocking SynchronizationThread in C# - Part 15: Local Storage
Thread in C# - Part 14: TimersThread in C# - Part 13: Asynchronous Delegates
Thread in C# - Part 12: Thread PoolingThread in C# - Part 11: ReaderWriterLock
  Xem tiếp    
 
Lịch khai giảng của hệ thống
 
Ngày
Giờ
T.Tâm
TP Hồ Chí Minh
Hà Nội
 
   
New ADSE - Nhấn vào để xem chi tiết
Mừng Sinh Nhật Lần Thứ 20 FPT-APTECH
Nhấn vào để xem chi tiết
Bảng Vàng Thành Tích Sinh Viên FPT APTECH - Nhấn vào để xem chi tiết
Cập nhật công nghệ miễn phí cho tất cả cựu sinh viên APTECH toàn quốc
Tiết Thực Vì Cộng Đồng
Hội Thảo CNTT
Những khoảnh khắc không phai của Thầy Trò FPT-APTECH Ngày 20-11