Monitor vs WaitHandle based thread sync
Asked Answered
B

3

34

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

Benefactor answered 31/8, 2009 at 1:3 Comment(0)
V
57

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
Vamp answered 31/8, 2009 at 1:14 Comment(6)
Thanks for explaining it, that makes perfect sense.Benefactor
I love this place... you can easily find and answer like this that will save you hours of researching through books and articles...Scevour
dtb - Can you please explain the 3rd bullet - Why it breaks with more than two threads?Memoir
The Monitor pulse/wait pattern is not hard if one loops on the Wait only while there's nothing to do, and does a PulseAll any time there's a change to anything another thread might be waiting on.Ceremony
@MiguelSevilla If you have 2 threads blocking on Monitor.Wait and a third thread sets / signals the event via Monitor.Pulse, only one of the waiting threads is gonna wake up because this code uses Monitor.Pulse instead of Monitor.PulseAll. This is because Monitor.Pulse wakes "at most one thread" while WaitHandles / the underlying Win32 event API release "all waiting threads". Note though, this does NOT mean "You can't implement ManualResetEvents with Monitors", only "This particular implementation doesn't fulfill the 'stickyness' property of ManualReset events".Actiniform
I think this answer is a bit misleading. Monitor.Pulse has equivalent behaviour to an AutoResetEvent. Implying that it is somehow unreliable or not solid because you are looking for ManualReset behaviour doesn't seem fair. If you want ManualReset (where the signal stays set), then use ManualResetEvent, but if you want AutoReset, then Monitor.Pulse can often be a nicer solution. You can simplify your code and avoid bugs that might occur if you dispose an explicit event at the wrong timeSoothe
C
3

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...

Coffeng answered 12/10, 2011 at 3:20 Comment(0)
W
-1

for @Will Gore's case, it is a good practice to always continue processing the queue until it is empty, before calling Monitor.Wait. E.g.:

while (keepgoing)
{ 
  List<Message> nextMsgs = new List<Message>();
  lock (messageQueue)
  {
    while (messageQueue.Count == 0)
    {
        try
        {
            Monitor.Wait(messageQueue);
        }
        catch(ThreadInterruptedException)
        {
            //...
        }
    }
    while (messageQueue.Count > 0)
        nextMsgs.Add(messageQueue.DeQueue());
  }
  if(nextMsgs.Count > 0)
    ProcessMessages(nextMsgs);
}

this should both solve the issue you met, and reduce the locking time (very important!).

Wobbly answered 10/1, 2017 at 22:44 Comment(2)
The variable nextMsg is declared, but never assigned. message is not declared, but assigned. If the two variables are supposed to be the same, only the last message will be processed - the rest will be discarded (pretty basic logic bug). Monitor.Wait is used outside the lock scope, potentially locking the queue forever. This is pretty bad.Installation
Thanks Kirill Shlenskiy for pointing out the mistakes and sorry about those. Already fixed. I was trying to show the idea to solve the 2 concerns the previous post had, but didn't get too careful. When Monitor.Wait is used outside lock, it will actually throw SynchronizationLockException.Wobbly

© 2022 - 2024 — McMap. All rights reserved.