In depth multithreading: a usage analysis of bidirectional signals and RACES

  • 2020-05-12 03:02:17
  • OfStack

Two-way signals and RACES (Two-Way Signaling and Races)

One important feature of the Monitor.Pulse method is that it is executed asynchronously, which means that calling the pulse method does not block itself waiting for Monitor.Pulse to return. If any one thread is waiting on the pulsed object, it will not block. In other words, calling Monitor.Pulse will have no effect on the program, and you can assume that the Monitor.Pulse method is ignored.
Thus Pulse provides one one-way communication: one pulsing thread silently sends signals to one waiting thread.
Pulse does not return a value that tells you if the waiting thread is signaled.

But sometimes we need to know if the waiting thread is being signaled, as in the following example:


class Race
    {
        static readonly object _locker = new object();
        static bool  _go;
        public static void MainThread()
        {
            new Thread(SaySomething).Start();
            for (int i = 0; i < 5; i++)
            {
                lock (_locker)
                {
                    _go = true;
                    Monitor.PulseAll(_locker); // Notify waiting queues 
                }
            }
        }
        static void SaySomething()
        {
            for (int i = 0; i < 5; i++)
            {
                lock (_locker)
                {
                    while (!_go) Monitor.Wait(_locker); // if _go  for false So start blocking. 
                    _go = false;
                    Console.WriteLine("Wassup?");
                }
            }
        }
    }

Expected output:
Wassup?
Wassup?
Wassup?
Wassup?
Wassup?

Actual output:

Wassup? (thread waiting)

In the SaySomething method, for loops to while, where _go is false, so Monitor.Wait begins to wait. In MainThread, the for loop sets _go to true. Then PulseAll. But the PulseAll method is asynchronous.
So the for loop in mainThread may have finished executing before the SaySomething thread is awakened. So the first Wait thread in the SaySomething method receives the message word _go for true, so go ahead and set the _go field to false again. Output "Wassup?" , but the next loop needs to wait again because _go is false. So the actual output prints 1 Wassup and then waits.
We need the main thread to block every iteration if the worker is still performing the last task. When worker is finished, the main thread resumes execution and then performs the iteration.

We can add a _ready flag to control the worker thread to ready before the _go flag is set on the main thread. That is, the main thread will wait for worker to complete the task before setting _go, and then wait for worker to set ready to true. When worker sets ready to true, the main thread will be notified via pulse.

class Race
    {
        static readonly object _locker = new object();
        static bool _ready, _go;
        public static void MainThread()
        {
            new Thread(SaySomething).Start();
            for (int i = 0; i < 5; i++)
            {
                lock (_locker)
                {
                    while (!_ready) Monitor.Wait(_locker); // if worker the ready for false , waiting for worker . 
                    _ready = false; // Reset the flag 
                    _go = true;
                    Monitor.PulseAll(_locker);
                }
            }
        }
        static void SaySomething()
        {
            for (int i = 0; i < 5; i++)
            {
                lock (_locker)
                {
                    _ready = true; // will ready Set to true
                    Monitor.PulseAll(_locker); // Notify the main thread, worker already ready We're ready to go. 
                    while (!_go) Monitor.Wait(_locker);
                    _go = false;
                    Console.WriteLine("Wassup?");
                }
            }
        }
    }


Related articles: