When ThreadB Monitor.Pulse(_locker) which thread will get _locker first?

Viewed 137

I read this article https://www.codeproject.com/Articles/28785/Thread-synchronization-Wait-and-Pulse-demystified#_articleTop

There is a quote:

Recommended pattern

These queues can lead to unexpected behaviour. When a Pulse occurs, the head of the waiting queue is released and is added to the ready queue. However, if there are other threads in the ready queue, they will acquire the lock before the thread that was released. This is a problem, because the thread that acquires the lock can alter the state that the pulsed thread relies on.

The solution is to use a while condition inside the lock statement:

readonly object key = new object();
bool block = true;

// thread A
lock ( key )
{
  while ( block )
    Monitor.Wait( key );
  block = true;
}

// thread B
lock ( key )
{
  block = false;
  Monitor.Pulse( key );
}

The author says ThreadC will get _locker first, but in my demo in which I run three threads I find it is not true. ThreadC gets _locker last.

Here is my code :

class Program
{
    private static readonly object _locker = new object();
    public static void ThreadA()
    {

        new Thread(() =>
        {
            lock (_locker)
            {
                Console.WriteLine("Thread A acquire lock then wait threadId {0}", Thread.CurrentThread.ManagedThreadId);
                Monitor.Wait(_locker);
                Thread.Sleep(1000);
                Console.WriteLine("Thread A  Continue .. threadId {0}", Thread.CurrentThread.ManagedThreadId);
            }
        }).Start();
            
    }
    public static void ThreadB()
    {

        new Thread(() =>
        {
            lock (_locker)
            {
                Console.WriteLine("Thread B acquire lock...then pulse  threadId {0}",Thread.CurrentThread.ManagedThreadId);
                Monitor.Pulse(_locker);
                Thread.Sleep(1000);
                Console.WriteLine("Thread B sleep...then realse the locker threadId {0}", Thread.CurrentThread.ManagedThreadId);
            }
        }).Start();

    }

    public static void ThreadC()
    {

        new Thread(ThreadA).Start();
    }

    static void Main(string[] args)
    {


        ThreadA();

        Thread.Sleep(10); // ensure threadA get _locker firstly

        ThreadB();

        ThreadC();


    }
}
1 Answers

The author says ThreadC will get _locker first

I don't think the author says that at all. The quote you posted reads:

if there are other threads in the ready queue, they will acquire the lock before the thread that was released

There is a very important "if" in that statement. Your code example does not appear to guarantee that thread C would be in the ready queue before thread A. In fact, by virtue of your ThreadC() method starting a whole new thread, which in turn then starts the ThreadA() thread again, thread C is even more likely to be delayed.

Of course, thread execution is entirely up to the Windows thread scheduler. Unless you add synchronization (such as the 10 ms delay you have before calling ThreadB()), the order of execution of your threads is subject to the whim of the thread scheduler. While typically threads will get scheduled round-robin, there's no guarantee of this.

But, even if we assume a guarantee of round-robin thread scheduling, I don't see anything in your code that would require or even suggest thread C would get in the monitor's ready queue before thread A does, never mind a statement from the author you're quoting that says that should be the case.

In the comments, you ask:

is there any chance threadC get firstly?

With the code you posted, this has no real chance of happening, because thread C has to get through two thread startups before it can try to acquire the lock. The odds of thread B failing to acquire the lock first and releasing thread A are slim to none. It is theoretically possible, but I doubt you've ever see it in practice. Even without thread C being two threads, it seems unlikely (though at least more plausible).

That said, it's interesting to consider that because thread C is only trying to acquire the lock, and never enters the wait queue, you can demonstrate a scenario where thread C arrives in the monitor's ready queue after thread A starts waiting, but before thread A becomes ready again, thus allowing thread C to acquire the lock before thread A gets it again.

I've modified your original code to show this, including adding an "elapsed seconds" to the output (so it's easier to see when everything happens), removing the "wrapper" thread that starts thread C, and adding parameters to help control and identify the threads:

using System.Diagnostics;
using System.Threading;
using static System.Console;

namespace TestSO46334766MonitorQueues
{
    class Program
    {
        private static readonly object _locker = new object();
        public static void ThreadA(string label, int delay, bool wait)
        {
            new Thread(() =>
            {
                LogMessage($"Thread {label} started, threadId {Thread.CurrentThread.ManagedThreadId}");
                Thread.Sleep(delay);
                LogMessage($"Thread {label} attempt to acquire lock, threadId {Thread.CurrentThread.ManagedThreadId}");
                lock (_locker)
                {
                    LogMessage($"Thread {label} acquire lock{(wait ? " then wait" : "")} threadId {Thread.CurrentThread.ManagedThreadId}");
                    if (wait) Monitor.Wait(_locker);
                    Thread.Sleep(1000);
                    LogMessage($"Thread {label} continue .. threadId {Thread.CurrentThread.ManagedThreadId}");
                }
            }).Start();
        }

        public static void ThreadB()
        {
            new Thread(() =>
            {
                LogMessage($"Thread B attempt to acquire lock, threadId {Thread.CurrentThread.ManagedThreadId}");
                lock (_locker)
                {
                    LogMessage($"Thread B acquire lock...sleep, then pulse threadId {Thread.CurrentThread.ManagedThreadId}");
                    Thread.Sleep(500);
                    Monitor.Pulse(_locker);
                    LogMessage($"Thread B sleep...then release the locker threadId {Thread.CurrentThread.ManagedThreadId}");
                    Thread.Sleep(1000);
                }
            }).Start();
        }

        public static void ThreadC()
        {
            //new Thread(() => ThreadA("C", 250, false)).Start();
            ThreadA("C", 250, false);
        }

        static void Main(string[] args)
        {
            ThreadA("A", 0, true);
            Thread.Sleep(10); // ensure threadA get _locker firstly
            ThreadB();
            ThreadC();
            ReadLine();
        }

        static Stopwatch sw = Stopwatch.StartNew();

        static void LogMessage(string message)
        {
            WriteLine($"{sw.Elapsed.TotalSeconds:0.0}: {message}");
        }
    }
}

Here's an example of the output:

0.1: Thread A started, threadId 3
0.1: Thread A attempt to acquire lock, threadId 3
0.1: Thread A acquire lock then wait threadId 3
0.2: Thread C started, threadId 5
0.2: Thread B attempt to acquire lock, threadId 4
0.2: Thread B acquire lock...sleep, then pulse threadId 4
0.4: Thread C attempt to acquire lock, threadId 5
0.7: Thread B sleep...then release the locker threadId 4
1.7: Thread C acquire lock threadId 5
2.7: Thread C continue .. threadId 5
3.7: Thread A continue .. threadId 3

Note in the above that thread C actually gets to run before thread B, in spite of thread B having been created first. This is an example of how the Windows thread scheduler does not guarantee order of thread execution.

Note also that while thread C starts at the same time as thread B, the first thing it does is sleep before actually trying to get the lock. This gives thread B a chance to get the lock first. Thus, when thread C does try to get the lock, it can't. But it does get put into the ready queue. Thread A is still in the wait queue, so when thread B finally releases it (by calling Pulse()), thread A winds up behind thread C in the ready queue. Once thread B finally leaves the lock, the threads in the ready queue get to acquire the lock in order, resulting in them also actually running in order.

Related