Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Monitor.Pulse & Wait - Unexpected Behaviour

http://www.codeproject.com/Articles/28785/Thread-synchronization-Wait-and-Pulse-demystified

Queues:

The ready queue is the collection of threads that are waiting for a particular lock. The Monitor.Wait methods introduce another queue: the waiting queue. This is required as waiting for a Pulse is distinct from waiting to acquire a lock. Like the ready queue, the waiting queue is FIFO.

Recommended pattern:

These queues can lead to unexpected behaviour. When a Pulse occurs, the head of the waiting queue is released and is added to the ready queue. However, if there are other threads in the ready queue, they will acquire the lock before the thread that was released. This is a problem, because the thread that acquires the lock can alter the state that the pulsed thread relies on. The solution is to use a while condition inside the lock statement

*Q = Queue.

By that, I understand that when I call Pulse, it does 2 things before it ends. Firstly, it removes one thread from the waiting Q to the ready Q. Secondly, it lets one thread (without knowing who is that thread) in the Ready Q acquire the lock; it doesn't care who acquires the lock (the thread that came from the waiting Q or a thread that was in the ready Q for some reason).

If I'm right about that then why is putting a while before Monitor.Wait helping to fix the problem (problem - the pulse ends even if the thread that came from the waiting Q didn't acquire the lock)?

A. Tell me if I'm right about the purpose of Monitor.Pulse.

B. Why do I need to put a while before Monitor.Wait

The full code of the answer below:

class Program
{
    static Queue<int> queue = new Queue<int>();
    static object someMonitor = new object();
    
    static void Main(string[] args)
    {
        Thread Thread1 = new Thread(WorkAlltheTime);
        Thread1.Name = "Thread1";
        Thread Thread2 = new Thread(WorkAlltheTime);
        Thread2.Name = "Thread2";
        Thread Thread3 = new Thread(WorkOnce);
        Thread3.Name = "Thread3";
        Thread1.Start();
        Thread2.Start();
        Thread.Sleep(1000);
        Thread3.Start();
        Console.ReadLine();
    }
    
    static void WorkAlltheTime()
    {
        Console.WriteLine("Came in to Ready Q: " + Thread.CurrentThread.Name);
        lock (someMonitor)
        {
            Console.WriteLine("Came out from Ready Q: " + Thread.CurrentThread.Name);
            // Broken!
            while (queue.Count == 0)
            {
                Console.WriteLine("Came in to Waiting Q: " + Thread.CurrentThread.Name);
                Monitor.Wait(someMonitor);
                Console.WriteLine("Came out from Waiting Q: " + Thread.CurrentThread.Name);
            }
            queue.Dequeue();
            Console.WriteLine("Thread: "+Thread.CurrentThread.Name+" Pulled Out");
        }
    }
    
    static void WorkOnce()
    {
        lock (someMonitor)
        {
            queue.Enqueue(1);
            Monitor.Pulse(someMonitor);
        }
    }   
}
like image 519
Stav Alfi Avatar asked Aug 25 '12 10:08

Stav Alfi


1 Answers

Imagine you're trying to write a producer/consumer queue - you Pulse each time you produce an item, and a consumer needs to wait until there's an item to consume. You'd write code like this:

Foo item;
lock(someMonitor)
{
    while (queue.Count == 0)
    {
        Monitor.Wait(someMonitor);
    }
    item = queue.Dequeue();
}
// Use the item

Suppose you didn't have the while loop, and instead wrote:

Foo item;
lock(someMonitor)
{
    // Broken!
    if (queue.Count == 0)
    {
        Monitor.Wait(someMonitor);
    }
    item = queue.Dequeue();
}
// Use the item

Now suppose you have one thread already waiting, and then another thread just before the lock statement... then a producer pulses the monitor (and adds an item to the queue, of course).

At that point, it's entirely feasible that the thread which hasn't even got to the lock yet will be the first to aquire the lock... at which point by the time the "waiting" thread acquires the lock, the queue would be empty again. With just a single if statement, without looping, you'd end up dequeuing when the queue is empty, which would fail.

With the while loop, you'll wait again until the next item is produced, which is what you really want.

like image 126
Jon Skeet Avatar answered Sep 27 '22 15:09

Jon Skeet