C #: Monitor - Wait, Pulse, PulseAll

I find it hard to understand Wait() , Pulse() , PulseAll() . Will they all avoid deadlocks? I would be grateful if you explain how to use them?

+41
multithreading c #
Oct 13 '09 at 10:08
source share
7 answers

Short version:

 lock(obj) {...} 

is short for Monitor.Enter / Monitor.Exit (with exception handling, etc.). If no one has a lock, you can get it (and run your code) - otherwise the thread will be blocked until the lock is received (by another thread that releases it).

A deadlock usually occurs when either A: two threads block things in different orders:

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

(here, if each of them receives the first lock, none of them can get the second, since no thread can exit to release their lock)

This scenario can be minimized by always fixing it in the same order; and you can restore (to the extent) using Monitor.TryEnter (instead of Monitor.Enter / lock ) and specify a timeout.

or B: you can block yourself with things like winforms when switching threads while holding the lock:

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

The dead end seems obvious above, but it's not so obvious when you have spaghetti code; Possible answers: do not switch threads while holding locks or use BeginInvoke so that you can at least get out of the lock (allow user interface playback).




Wait / Pulse / PulseAll different; They are intended for signaling. I use this in this answer to signal like this:

  • Dequeue : if you try to remove data from the queue when the queue is empty, it waits for another thread to add data that wakes up the blocked thread
  • Enqueue : if you try to insert data into the queue when the queue is full, it expects another thread to delete the data that wakes up the blocked thread

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

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

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

+47
Oct 13 '09 at 11:32
source share

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

 object phone = new object(); 

Theme "Worker":

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

Theme "Boss":

 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":

 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(); } } 

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!"); } 
+36
Mar 21 '12 at 17:58
source share

No, they do not protect you from dead ends. These are simply more flexible tools for synchronizing threads. Here is a very good explanation of how to use them and a very important usage pattern - without this template you will break everything: http://www.albahari.com/threading/part4.aspx

+7
Oct 13 '09 at 10:12
source share

Read the multi-country Thread Threading article .

This is really good. The ones you mention are about a third of the way.

+1
Oct 13 '09 at 10:10
source share

These are tools for synchronization and signaling between threads. Thus, they do nothing to prevent deadlocks, but if used correctly, they can be used to synchronize and exchange data between threads.

Unfortunately, most of the work required to write the correct multithreaded code is currently the responsibility of developers in C # (and many other languages). See how F #, Haskell, and Clojure handle this for a completely different approach.

+1
Oct 13 '09 at 10:14
source share

Unfortunately, none of Wait (), Pulse (), or PulseAll () has the magic property that you want - that with this API you automatically avoid a deadlock.

Consider the following code

 object incomingMessages = new object(); //signal object LoopOnMessages() { lock(incomingMessages) { Monitor.Wait(incomingMessages); } if (canGrabMessage()) handleMessage(); // loop } ReceiveMessagesAndSignalWaiters() { awaitMessages(); copyMessagesToReadyArea(); lock(incomingMessages) { Monitor.PulseAll(incomingMessages); //or Monitor.Pulse } awaitReadyAreaHasFreeSpace(); } 

This code will close! Maybe not today, maybe not tomorrow. Most likely, when your code is under stress, because suddenly it has become popular or important, and you are called in to fix an urgent problem.

Why?

In the end, the following will happen:

  • All consumer flows do some work.
  • Messages arrive, the ready area cannot contain more messages, and PulseAll () is called.
  • No user wakes up because no one is waiting
  • All consumer flows are called Wait () [DEADLOCK]

This particular example assumes that the manufacturer thread will never call PulseAll () again, because it no longer has space to enter messages. But there are many, many broken versions of this code. People will try to make it more reliable by changing the line, for example, by making Monitor.Wait(); at

 if (!canGrabMessage()) Monitor.Wait(incomingMessages); 

Unfortunately, this is still not enough to fix it. To fix this, you also need to change the blocking area in which Monitor.PulseAll() is called:

 LoopOnMessages() { lock(incomingMessages) { if (!canGrabMessage()) Monitor.Wait(incomingMessages); } if (canGrabMessage()) handleMessage(); // loop } ReceiveMessagesAndSignalWaiters() { awaitMessagesArrive(); lock(incomingMessages) { copyMessagesToReadyArea(); Monitor.PulseAll(incomingMessages); //or Monitor.Pulse } awaitReadyAreaHasFreeSpace(); } 

The key point is that in a fixed code, locks limit the possible sequence of events:

  • Consumer flows do their work and cycles

  • This thread gets blocked

    And thanks to the lock, it’s now true that either:

  • but. Messages have not yet arrived at the ready area, and it releases the lock by calling Wait () BEFORE the message receiver thread can receive the lock and copy more messages to the ready area or

    b. Messages already arrive in the ready area and receive INSTEAD OF messages that call Wait (). (And although he makes this decision, it is impossible for the message recipient thread, for example, to acquire a lock and copy more messages to the ready area.)

As a result, the source code problem now never arises: 3. When PulseEvent () is called. You can’t wake a consumer because no one is waiting

Now pay attention to the fact that in this code you need to precisely fix the blocking area. (If I'm really right!)

And also, since you must use lock (or Monitor.Enter() , etc.) to use Monitor.PulseAll() or Monitor.Wait() without locking, you still have to worry about the possibility of other deadlocks that come from -for this lock.

Bottom line: these APIs are also easy to screw and slow down, i.e. quite dangerous

+1
Apr 15 '15 at 10:50
source share

Something that threw me away is that Pulse just gives heads-up to a stream in Wait . The wait thread will not continue until the thread that Pulse done does not issue a lock, and the waiting thread wins successfully.

 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, this only happens until the “lock on the phone has been removed”, which can be received by another thread.

There may be other threads waiting for locks from Monitor.Wait(phone) or lock(phone) . Only the one who wins the block will continue.

0
03 Mar. '17 at 14:25
source share



All Articles