Domanda

Here is a minimal code that illustrates the issue:

StringBuilder input = new StringBuilder();

void ToUpper()
{
    lock (input)
    {
        while (true)
        {
            Monitor.Wait(input);

            Console.WriteLine(input.ToString().ToUpper());
        }
    }
}

public void Run()
{
    new Thread(ToUpper) { IsBackground = true }.Start();

    // "Avoid" the initial race
    Thread.Sleep(100);

    while (true) 
    {
        lock (input)
        {
            input.Clear();
            input.Append(Console.ReadLine());
            Monitor.Pulse(input);
        }
        // Thread.Sleep(1);
    }
}

Ignoring the well-known initial race-condition, I'm surprised by the behavior of Pulse and Wait.

Here is what I expected:

  • the "ToUpper" thread calls Wait => it is pushed into the monitor's waiting queue
  • the main thread calls Pulse => the "ToUpper" thread is "moved" from the waiting queue to the ready queue for immediate acquisition of the lock
  • the main thread exits the monitor when exiting the lock statement scope
  • the "ToUpper" thread acquires the lock and processes input while the main thread is registering again for ownership of the lock

But one time out of two the "ToUpper" thread does not process the input and instead the main thread executes its processing immediately.

Here are with my assumptions:

  • Pulse does not "move" the "ToUpper" thread immediately so the ready queue stays empty
  • the main thread releases the lock, loops, requires the lock again
  • as there is nobody else in the ready queue it takes ownership
  • sometimes later the request for promoting the "ToUpper" thread is executed and it is finally pushed into the ready queue
  • the main thread Pulse a second time for nothing and releases the lock
  • it loops, tries to reacquire the lock but the "ToUpper" thread is already there
  • this time the "ToUpper" thread acquires the lock and process the input
  • once done it sleeps again waiting for the next signal
  • the main thread acquires the lock

To check this assumption I've forced the main thread to be less eager and to let the other thread work, by adding the Thread.Sleep(1) => in this case all is "working as expected".

So it all boils down to the behavior of Pulse that might not push immediately the threads from the waiting queue to the ready queue.

Does the issue really comes from this possible race or is there another subtlety I'm missing?

È stato utile?

Soluzione

Your remark about adding Thread.Sleep(1) is really the answer. The Pulse method doesn't guarantee anything for the waiting object - it is just released to the ready queue from which it proceeds as an ordinary thread. There is no trace anywhere about the call to the Pulse. Thus, after you call Pulse the application behaves as ordinary two-threaded application, and there are two threads in the ready queue - the ToUpper thread and the Run thread. So without Thread.Sleep(1) it can happen (without it it's also possible I guess, just less probable) that the Run thread obtains the lock first.

Another important remark from the second link I've provided:

An important feature of Monitor.Pulse is that it executes asynchronously, meaning that it doesn't itself block or pause in any way.

For this scenario the AutoResetEvent class seems more suitable. Also, in the second link you can find an example of a producer-consumer scenario with Wait and Pulse.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top