Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Monitor vs WaitHandle based thread sync

I was under the impression, after reading this article that it is better to use Monitor/Lock for thread synchronisation as it does not use native resources

Specific quote (from page 5 of the article):

Monitor.Wait/Pulse isn't the only way of waiting for something to happen in one thread and telling that thread that it's happened in another. Win32 programmers have been using various other mechanisms for a long time, and these are exposed by the AutoResetEvent, ManualResetEvent and Mutex classes, all of which derive from WaitHandle. All of these classes are in the System.Threading namespace. (The Win32 Semaphore mechanism does not have a managed wrapper in .NET 1.1. It's present in .NET 2.0, but if you need to use it before then, you could either wrap it yourself using P/Invoke, or write your own counting semaphore class.)

Some people may be surprised to learn that using these classes can be significantly slower than using the various Monitor methods. I believe this is because going "out" of managed code into native Win32 calls and back "in" again is expensive compared with the entirely managed view of things which Monitor provides. A reader has also explained that monitors are implemented in user mode, whereas using wait handles require switching into kernel mode, which is fairly expensive.

But since discovering SO and reading a few of the questions/answers here I have started to doubt my understanding of when to use each. It seems that many people recommend using Auto/ManualResetEvent in the cases where a Monitor.Wait/Pulse would do. Can anyone explain to me when WaitHandle based sync should be used over Monitor?

Thanks

like image 565
Matt Avatar asked Aug 31 '09 01:08

Matt


2 Answers

A problem with Monitor.Pulse/Wait is that the signal may get lost.

For example:

var signal = new ManualResetEvent(false);

// Thread 1
signal.WaitOne();

// Thread 2
signal.Set();

This will always work no matter in which the two statements in the different threads are executed. It's also a very clean abstraction and expresses very clearly your intent.

Now have a look at the same example using a monitor:

var signal = new object();

// Thread 1
lock (signal)
{
    Monitor.Wait(signal);
}

// Thread 2
lock (signal)
{
    Monitor.Pulse(signal);
}

Here the signal (Pulse) will get lost if Pulse is executed before Wait.

To fix this problem, you need something like this:

var signal = new object();
var signalSet = false;

// Thread 1
lock (signal)
{
    while (!signalSet)
    {
        Monitor.Wait(signal);
    }
}

// Thread 2
lock (signal)
{
    signalSet = true;
    Monitor.Pulse(signal);
}

This works and is probably even more performant and lightweight, but is less readable. And it's where the headache called concurrency starts.

  • Does this code really work?
  • In every corner case?
  • With more than two threads? (Hint: it doesn't)
  • How do you unit-test it?

A solid, reliable, readable abstraction is often better than raw performance.

Additionally, WaitHandles provide some nice stuff like waiting for a set of handles to be set, etc. Implementing this with monitors makes the headache even worse...


Rule of thumb:

  • Use Monitors (lock) to ensure exclusive access to a shared resource
  • Use WaitHandles (Manual/AutoResetEvent/Semaphore) to send signals between threads
like image 53
dtb Avatar answered Nov 13 '22 22:11

dtb


I think I have a pretty good example of the 3rd bullet (and while this thread is a bit old, it might help someone).

I've some code where thread A receives network messages, enqueues them, then pulses thread B. Thread B locks, dequeues any messages, unlocks the queue, then processes the messages.

The problem arrives in that while Thread B is processing, and is not waiting, if A gets a new network message, enqueues and pulses... well, B isn't waiting so the pulse just evaporates. If B then finishes what it was doing and hits the Monitor.Wait(), then the recently added message will just hang around until another message arrives and the pulse is received.

Note that this problem didn't really surface for a while, as originally my entire loop was something like:

while (keepgoing)
  { 
  lock (messageQueue)
      {
      while (messageQueue.Count > 0)
          ProcessMessages(messageQueue.DeQueue());

      Monitor.Wait(messageQueue);
      }
  }

This problem didn't crop up (well, there were rare oddities on shutdown, so I was a bit suspicious of this code) until I decided that the (potentially long running) message processing shouldn't keep the queue locked as it had no reason to. So I changed it to dequeue the messages, leave the lock, THEN do the processing. And then it seemed like I started missing messages, or they would only arrive after a second event happened...

like image 3
Will Gore Avatar answered Nov 14 '22 00:11

Will Gore