C# : Monitor - Wait,Pulse,PulseAll

I am having hard time in understanding Wait(), Pulse(), PulseAll(). Will all of them avoid deadlock? I would appreciate if you explain how to use them?


Solution 1:

Short version:

lock(obj) {...}

is short-hand for Monitor.Enter / Monitor.Exit (with exception handling etc). If nobody else has the lock, you can get it (and run your code) - otherwise your thread is blocked until the lock is aquired (by another thread releasing it).

Deadlock typically happens when either A: two threads lock things in different orders:

thread 1: lock(objA) { lock (objB) { ... } }
thread 2: lock(objB) { lock (objA) { ... } }

(here, if they each acquire the first lock, neither can ever get the second, since neither thread can exit to release their lock)

This scenario can be minimised by always locking in the same order; and you can recover (to a degree) by using Monitor.TryEnter (instead of Monitor.Enter/lock) and specifying a timeout.

or B: you can block yourself with things like winforms when thread-switching while holding a lock:

lock(obj) { // on worker
    this.Invoke((MethodInvoker) delegate { // switch to UI
        lock(obj) { // oopsiee!
            ...
        }
    });
}

The deadlock appears obvious above, but it isn't so obvious when you have spaghetti code; possible answers: don't thread-switch while holding locks, or use BeginInvoke so that you can at least exit the lock (letting the UI play).


Wait/Pulse/PulseAll are different; they are for signalling. I use this in this answer to signal so that:

  • Dequeue: if you try to dequeue data when the queue is empty, it waits for another thread to add data, which wakes up the blocked thread
  • Enqueue: if you try and enqueue data when the queue is full, it waits for another thread to remove data, which wakes up the blocked thread

Pulse only wakes up one thread - but I'm not brainy enough to prove that the next thread is always the one I want, so I tend to use PulseAll, and simply re-verify the conditions before continuing; as an example:

        while (queue.Count >= maxSize)
        {
            Monitor.Wait(queue);
        }

With this approach, I can safely add other meanings of Pulse, without my existing code assuming that "I woke up, therefore there is data" - which is handy when (in the same example) I later needed to add a Close() method.

Solution 2:

Simple recipe for use of Monitor.Wait and Monitor.Pulse. It consists of a worker, a boss, and a phone they use to communicate:

object phone = new object();

A "Worker" thread:

lock(phone) // Sort of "Turn the phone on while at work"
{
    while(true)
    {
        Monitor.Wait(phone); // Wait for a signal from the boss
        DoWork();
        Monitor.PulseAll(phone); // Signal boss we are done
    }
}

A "Boss" thread:

PrepareWork();
lock(phone) // Grab the phone when I have something ready for the worker
{
    Monitor.PulseAll(phone); // Signal worker there is work to do
    Monitor.Wait(phone); // Wait for the work to be done
}

More complex examples follow...

A "Worker with something else to do":

lock(phone)
{
    while(true)
    {
        if(Monitor.Wait(phone,1000)) // Wait for one second at most
        {
            DoWork();
            Monitor.PulseAll(phone); // Signal boss we are done
        }
        else
            DoSomethingElse();
    }
}

An "Impatient Boss":

PrepareWork();
lock(phone)
{
    Monitor.PulseAll(phone); // Signal worker there is work to do
    if(Monitor.Wait(phone,1000)) // Wait for one second at most
        Console.Writeline("Good work!");
}

Solution 3:

No, they don't protect you from deadlocks. They are just more flexible tools for thread synchronization. Here is a very good explanation how to use them and very important pattern of usage - without this pattern you will break all the things: http://www.albahari.com/threading/part4.aspx

Solution 4:

Something that total threw me here is that Pulse just gives a "heads up" to a thread in a Wait. The Waiting thread will not continue until the thread that did the Pulse gives up the lock and the waiting thread successfully wins it.

lock(phone) // Grab the phone
{
    Monitor.PulseAll(phone); // Signal worker
    Monitor.Wait(phone); // ****** The lock on phone has been given up! ******
}

or

lock(phone) // Grab the phone when I have something ready for the worker
{
    Monitor.PulseAll(phone); // Signal worker there is work to do
    DoMoreWork();
} // ****** The lock on phone has been given up! ******

In both cases it's not until "the lock on phone has been given up" that another thread can get it.

There might be other threads waiting for that lock from Monitor.Wait(phone) or lock(phone). Only the one that wins the lock will get to continue.