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.