锁之锁的三种状态及Monitor.Wait, Monitor.Pulse,Monitor.PulseAll的作用与用法
目录
1.线程锁的三种状态
2.Monitor.Wait,Monitor.Pulse的作用
3.Monitor.PulseAll的作用
4.总结(Q&A)
首先我们记住有有锁线程的三种状态:[持有锁的线程],[就绪队列],[等待队列]
微软官方文档中英文截图释义如下[2][3]:
整理后中英文及释义对照表如下:
中文 |
英文 |
释义 |
[等待队列] |
waiting queue |
线程运行到Wait处后状态被改为的等待队列,需要进入就绪队列才拿到锁 |
[就绪队列] |
ready queue |
即将进入执行中的带锁的线程 |
[持有锁的线程] |
the thread that |
执行中的带锁的线程 |
软件中运行优先顺序为:
1.先运行 [持有锁的线程] ,
2.然后是 [就绪队列] 中排在最前的线程。
3.只有[等待队列] 的线程进入[就绪队列]才有排队资格,且排到最前才会进入[持有锁的线程]
需要注意:
1. 当前只能有一个线程状态为[持有锁的线程]
2. 前一个线程释放了锁,才可排到[就绪队列]中最前的线程
3. 若[等待队列]一直进入不了[就绪队列]则一直不计入排队序列
以超市购物为例,
同个结账窗口中:
1.当前窗口只能有一个客户在结账
2.只有前一个客户结完账后,排队队伍中排到最前的客户才可结账
3.超市里面排队采购,排队结账,若不排队则不能结账
若结账窗口是锁,则:
1. [持有锁的线程] 即 当前窗口结帐中的顾客(结账中)
2. [就绪队列] 即 排队准备结账的顾客们(排队中)
3. [等待队列] 即 仍在超市采购的顾客们(有顺序的采购中)
前文已说过有锁线程的三种状态:[持有锁的线程],[就绪队列],[等待队列]
软件中运行优先顺序为:
1.先运行 [持有锁的线程] ,
2.然后是 [就绪队列] 中排在最前的线程。
3.只有[等待队列] 的线程进入[就绪队列]才有排队资格,且排到最前才会进入[持有锁的线程]
以上记好
那么Monitor.Wait和Monitor.Pulse是什么意思呢?
Visual Studio中直接按F12或右键->转到定义,我们发现官方解释是如下
其中Monitor.Pulse的解释为:
//摘要: 通知等待队列中的线程锁定对象状态的更改。
//Summary:Notifies a thread in the waiting queue of a change in the locked object's state.
Monitor.Wait中英文释义如下:
//摘要: 释放对象上的锁并阻止当前线程,直到它重新获取该锁。如果指定的超时间隔已过,则线程进入就绪队列。
//Summary:Releases the lock on an object and blocks the current thread until it reacquires the lock.
不知道你们看懂没,我看上面的摘要的中文看的云里雾里的,英文看的迷迷糊糊的,
按F12或右键->转到定义 相关截图如下:
然后查好久查到如下微软官方文档[2][3]:
从微软官方文档中总结出:
Wait 作用是:将[持有锁的线程]的本线程状态改为[等待队列]
Pulse作用是:将排在[等待队列] 最前的线程 移到[就绪队列]
前面我们举过例子:
1. [持有锁的线程] 即(结账中)
2. [就绪队列] 即(排队中)
3. [等待队列] 即(有顺序的采购中)
然后
Wait 作用是:将当前结账的顾客拉回到超市里面瞎逛,
即当前状态从(结账中)转为(有顺序的采购中)。
Pulse作用是:将在超市里瞎逛的顾客拉去排队,
即当前状态从排在最前的(有顺序的采购中)状态转为(排队中)。
微软官网没明说,那就说明Pulse后转到[就绪队列]的线程很可能会插队。
那么又有新的疑问:
<问1>若有多个wait同时起作用,那么多个线程状态在[等待队列]中的排序是?
<问2> 单个Pulse作用是将[等待队列] 最前的线程 移到[就绪队列]的第几位?
上面两个问题微软官网查了好久,没查到
我们来写个测试程序测试下:
开6个线程其中[线程A]和[线程B]中有Wait,[线程C]中有Pulse,通过控制Thread.Sleep来控制线程到达[就绪队列]的顺序为A, B,C,D,E,F。
部分代码如下:
private void button7_Click(object sender, EventArgs e)
{
this.Hide();
Console.WriteLine("[主线程] " + DateTime.Now.ToString() + "开启子线程");
Thread aa = new Thread(A); aa.IsBackground = true; aa.Name = "[线程A]"; aa.Start();
Thread bb = new Thread(B); bb.IsBackground = true; bb.Name = "[线程B]"; bb.Start();
Thread cc = new Thread(C); cc.IsBackground = true; cc.Name = "[线程C]"; cc.Start();
Thread dd = new Thread(D); dd.IsBackground = true; dd.Name = "[线程D]"; dd.Start();
Thread ee = new Thread(E); ee.IsBackground = true; ee.Name = "[线程E]"; ee.Start();
Thread ff = new Thread(F); ff.IsBackground = true; ff.Name = "[线程F]"; ff.Start();
Console.ReadLine();
}
private static object objLockTest = new object();
private static void A()
{
Console.WriteLine("[线程A] 开启...");
Thread.Sleep(100);
Console.WriteLine("[线程A] [就绪队列]");
lock (objLockTest) //进入[就绪队列]队列
{
Console.WriteLine("[线程A] [持有锁的线程]");
Thread.Sleep(1000);
Console.WriteLine("[线程A] Monitor.Pulse," + DateTime.Now.ToString("HH:mm:ss:fff"));
//Notifies a thread in the waiting queue of a change in the locked object's state.
Monitor.Pulse(objLockTest);
Thread.Sleep(100);
Console.WriteLine("[线程A] Monitor.Wait ," + DateTime.Now.ToString("HH:mm:ss:fff"));
//Releases the lock on an object and blocks the current thread until it reacquires the lock.
//(Monitor.Wait将本线程添加到[等待队列]
//超时时间:线程进入就绪队列之前等待的毫秒数,即从Wait 到Pulse之间的毫秒数,
// 若无超时时间,且无Pulse唤醒,则本线程一直会处于[等待队列]
if (Monitor.Wait(objLockTest, 3000))
Console.WriteLine("[线程A] 重新获得该锁(在指定的时间过期之前)" + DateTime.Now.ToString("HH:mm:ss:fff"));
else
Console.WriteLine("[线程A] 重新获得该锁(在指定的时间过期之后)" + DateTime.Now.ToString("HH:mm:ss:fff"));
Thread.Sleep(4000);
Console.WriteLine("[线程A] 退出..." + DateTime.Now.ToString("HH:mm:ss:fff"));
}
}
private static void B()
{
Console.WriteLine("[线程B] 开启...");
Thread.Sleep(150);
Console.WriteLine("[线程B] [就绪队列]");
lock (objLockTest)
{
Console.WriteLine("[线程B] [持有锁的线程]");
Thread.Sleep(100);
Console.WriteLine("[线程B] Monitor.Wait," + DateTime.Now.ToString("HH:mm:ss:fff"));
if (Monitor.Wait(objLockTest, 3000))
Console.WriteLine("[线程B] 重新获得该锁(在指定的时间过期之前)" + DateTime.Now.ToString("HH:mm:ss:fff"));
else
Console.WriteLine("[线程B] 重新获得该锁(在指定的时间过期之后)" + DateTime.Now.ToString("HH:mm:ss:fff"));
Thread.Sleep(500);
Console.WriteLine("[线程B] 退出...");
}
}
private static void C()
{
Console.WriteLine("[线程C] 开启...");
Thread.Sleep(200);
Console.WriteLine("[线程C] [就绪队列]");
lock (objLockTest)
{
Console.WriteLine("[线程C] [持有锁的线程]");
Thread.Sleep(100);
Console.WriteLine("[线程C] Monitor.Pulse," + DateTime.Now.ToString("HH:mm:ss:fff"));
//Notifies a thread in the waiting queue of a change in the locked object's state.
Monitor.Pulse(objLockTest);
Thread.Sleep(500);
Console.WriteLine("[线程C] 退出...");
}
}
private static void D()
{
Console.WriteLine("[线程D] 开启...");
Thread.Sleep(250);
Console.WriteLine("[线程D] [就绪队列]");
lock (objLockTest)
{
Console.WriteLine("[线程D] [持有锁的线程]");
Thread.Sleep(500);
Console.WriteLine("[线程D] 退出...");
}
}
private static void E()
{
Console.WriteLine("[线程E] 开启...");
Thread.Sleep(300);
Console.WriteLine("[线程E] [就绪队列]");
lock (objLockTest)
{
Console.WriteLine("[线程E] [持有锁的线程]" + DateTime.Now.ToString("HH:mm:ss:fff"));
Thread.Sleep(500);
Console.WriteLine("[线程E] 退出...");
}
}
private static void F()
{
Console.WriteLine("[线程F] 开启...");
Thread.Sleep(350);
Console.WriteLine("[线程F] [就绪队列]");
lock (objLockTest)
{
Console.WriteLine("[线程F] [持有锁的线程]" + DateTime.Now.ToString("HH:mm:ss:fff"));
Thread.Sleep(500);
Console.WriteLine("[线程F] 退出...");
}
}
软件运行结果及对应释义图如下:
上图中绿色画线处为Wait ,Pulse使用后的结果的释义。
我们设置线程到达[就绪队列]状态的顺序为:A-> B->C->D->E->F。
[线程A]中有代码if (Monitor.Wait(objLockTest, 3000))
[线程B]中有代码if (Monitor.Wait(objLockTest, 3000))
[线程C]中有代码Monitor.Pulse(objLockTest);
则线程状态变为[持有锁的线程] 顺序为:A(Pulse,Wait)->B(Wait)->C(Pulse)->D->A->E->F
从刚程序中线程[持有锁的线程] 顺序的状态变化我们得出了如下结论:
结论1. 线程中若最先运行的Pulse,则此Pulse无作用。
结论2. 当前运行的线程Wait后会释放本线程的锁并将本线程移到[等待队列]
结论3. 无论当前运行的线程是否调用Pulse,下一个都会运行到原来就排在[就绪队列]首位的线程。
结论4. 线程Pulse后将[等待队列] 最前的线程 移到[就绪队列], 即默认将第一个调用Wait的线程移动到[就绪队列]
结论5. Wait的超时时间统计的是[等待队列]到[就绪队列]所用的时间,即调用Wait的时间与Pulse的时间差,若时间差小于超时时间,则返回True,反之,返回False。
结论6.单个Pulse运行将 [等待队列] 最前的线程 大概率插队到[就绪队列]的第二位(此结论是从结论3基础上以逻辑方式推理出来的)
那么以上两个问题,我们都有了解释
<问1>若有多个wait同时起作用,那么多个线程状态在[等待队列]中的排序是?
<答1>多个wait起作用后多个线程依次进入[等待队列]状态,即最先进入[等待队列]的排在最前。
<问2> 单个Pulse作用是将[等待队列] 最前的线程 移到[就绪队列]的第几位?
<答2>单个Pulse运行后从本程序上看,应该是[等待队列] 最前的线程 大概率插队到[就绪队列]的第二位。
以上结论与微软官方文档释义相同,
官方文档截图如下[4][5]:
附赠软件运行顺序及运行后各个线程的状态图表:
|
[持有锁的线程] |
[就绪队列] |
[等待队列] |
[主线程] 2020/4/13 13:32:03开启子线程 |
|
|
|
[线程C] 开启... |
|
|
|
[线程A] 开启... |
|
|
|
[线程D] 开启... |
|
|
|
[线程E] 开启... |
|
|
|
[线程B] 开启... |
|
|
|
[线程F] 开启... |
|
|
|
[线程A] [就绪队列] |
|
A |
|
[线程A] [持有锁的线程] |
A |
|
|
[线程B] [就绪队列] |
A |
B |
|
[线程C] [就绪队列] |
A |
B,C |
|
[线程D] [就绪队列] |
A |
B,C,D |
|
[线程E] [就绪队列] |
A |
B,C,D,E |
|
[线程F] [就绪队列] |
A |
B,C,D,E,F |
|
[线程A] Monitor.Pulse,13:32:04:161 |
A |
B,C,D,E,F |
|
[线程A] Monitor.Wait ,13:32:04:271 |
|
B,C,D,E,F |
A |
[线程B] [持有锁的线程] |
B |
C,D,E,F |
A |
[线程B] Monitor.Wait,13:32:04:380 |
|
C,D,E,F |
A,B |
[线程C] [持有锁的线程] |
C |
D,E,F |
A,B |
[线程C] Monitor.Pulse,13:32:04:489 |
C |
D,A,E,F |
B |
[线程C] 退出... |
|
D,A,E,F |
B |
[线程D] [持有锁的线程] |
D |
A,E,F |
B |
[线程D] 退出... |
|
A,E,F |
B |
[线程A] 重新获得该锁(在指定的时间过期之前)13:32:05:520 |
A |
E,F |
B |
软件的[线程B]中Wait超时时间3000ms |
A |
E,B,F |
|
[线程A] 退出...13:32:09:535 |
|
E,B,F |
|
[线程E] [持有锁的线程]13:32:09:535 |
E |
B,F |
|
[线程E] 退出... |
|
B,F |
|
[线程B] 重新获得该锁(在指定的时间过期之后)13:32:10:051 |
B |
F |
|
[线程B] 退出... |
|
F |
|
[线程F] [持有锁的线程]13:32:10:566 |
F |
|
|
[线程F] 退出... |
|
|
|
<问 6>请问Monitor.PulseAll具体的作用是?
多说无益代码验证如下
改为先开启线程C,即先运行cc.Start();
[线程A]中if (Monitor.Wait(objLockTest, 300))
[线程B]中if (Monitor.Wait(objLockTest, 3000))
[线程C]中if (Monitor.Wait(objLockTest, 3000))
[线程D]中Monitor.PulseAll(objLockTest),然后查看软件运行变化,
改后代码:
private void button7_Click(object sender, EventArgs e)
{
this.Hide();
Console.WriteLine("[主线程] " + DateTime.Now.ToString() + "开启子线程");
Thread cc = new Thread(C); cc.IsBackground = true; cc.Name = "[线程C]"; cc.Start();
Thread aa = new Thread(A); aa.IsBackground = true; aa.Name = "[线程A]"; aa.Start();
Thread bb = new Thread(B); bb.IsBackground = true; bb.Name = "[线程B]"; bb.Start();
Thread dd = new Thread(D); dd.IsBackground = true; dd.Name = "[线程D]"; dd.Start();
Thread ee = new Thread(E); ee.IsBackground = true; ee.Name = "[线程E]"; ee.Start();
Thread ff = new Thread(F); ff.IsBackground = true; ff.Name = "[线程F]"; ff.Start();
Console.ReadLine();
}
private static object objLockTest = new object();
private static void A()
{
Console.WriteLine("[线程A] 开启...");
Thread.Sleep(100);
Console.WriteLine("[线程A] [就绪队列]");
lock (objLockTest) //进入[就绪队列]队列
{
Console.WriteLine("[线程A] [持有锁的线程]");
Thread.Sleep(1000);
Console.WriteLine("[线程A] Monitor.Pulse," + DateTime.Now.ToString("HH:mm:ss:fff"));
//Notifies a thread in the waiting queue of a change in the locked object's state.
Monitor.Pulse(objLockTest);
Thread.Sleep(100);
Console.WriteLine("[线程A] Monitor.Wait ," + DateTime.Now.ToString("HH:mm:ss:fff"));
//Releases the lock on an object and blocks the current thread until it reacquires the lock.
//(Monitor.Wait将本线程添加到[等待队列]
//超时时间:线程进入就绪队列之前等待的毫秒数,即从Wait 到Pulse之间的毫秒数,
// 若无超时时间,且无Pulse唤醒,则本线程一直会处于[等待队列]
if (Monitor.Wait(objLockTest, 300))
Console.WriteLine("[线程A] 重新获得该锁(在指定的时间过期之前)" + DateTime.Now.ToString("HH:mm:ss:fff"));
else
Console.WriteLine("[线程A] 重新获得该锁(在指定的时间过期之后)" + DateTime.Now.ToString("HH:mm:ss:fff"));
Thread.Sleep(4000);
Console.WriteLine("[线程A] 退出..." + DateTime.Now.ToString("HH:mm:ss:fff"));
}
}
private static void B()
{
Console.WriteLine("[线程B] 开启...");
Thread.Sleep(150);
Console.WriteLine("[线程B] [就绪队列]");
lock (objLockTest)
{
Console.WriteLine("[线程B] [持有锁的线程]");
Thread.Sleep(100);
Console.WriteLine("[线程B] Monitor.Wait," + DateTime.Now.ToString("HH:mm:ss:fff"));
if (Monitor.Wait(objLockTest, 3000))
Console.WriteLine("[线程B] 重新获得该锁(在指定的时间过期之前)" + DateTime.Now.ToString("HH:mm:ss:fff"));
else
Console.WriteLine("[线程B] 重新获得该锁(在指定的时间过期之后)" + DateTime.Now.ToString("HH:mm:ss:fff"));
Thread.Sleep(500);
Console.WriteLine("[线程B] 退出...");
}
}
private static void C()
{
Console.WriteLine("[线程C] 开启...");
Thread.Sleep(130);
Console.WriteLine("[线程C] [就绪队列]");
lock (objLockTest)
{
Console.WriteLine("[线程C] [持有锁的线程]");
Thread.Sleep(100);
Console.WriteLine("[线程C] Monitor.Wait," + DateTime.Now.ToString("HH:mm:ss:fff"));
if (Monitor.Wait(objLockTest, 3000))
Console.WriteLine("[线程C] 重新获得该锁(在指定的时间过期之前)" + DateTime.Now.ToString("HH:mm:ss:fff"));
else
Console.WriteLine("[线程C] 重新获得该锁(在指定的时间过期之后)" + DateTime.Now.ToString("HH:mm:ss:fff"));
Thread.Sleep(500);
Console.WriteLine("[线程C] 退出...");
}
}
private static void D()
{
Console.WriteLine("[线程D] 开启...");
Thread.Sleep(250);
Console.WriteLine("[线程D] [就绪队列]");
lock (objLockTest)
{
Console.WriteLine("[线程D] [持有锁的线程]");
Thread.Sleep(100);
Console.WriteLine("[线程D] Monitor.PulseAll, " + DateTime.Now.ToString("HH:mm:ss:fff"));
//Notifies a thread in the waiting queue of a change in the locked object's state.
Monitor.PulseAll(objLockTest);
Thread.Sleep(500);
Console.WriteLine("[线程D] 退出...");
}
}
private static void E()
{
Console.WriteLine("[线程E] 开启...");
Thread.Sleep(300);
Console.WriteLine("[线程E] [就绪队列]");
lock (objLockTest)
{
Console.WriteLine("[线程E] [持有锁的线程]" + DateTime.Now.ToString("HH:mm:ss:fff"));
Thread.Sleep(500);
Console.WriteLine("[线程E] 退出...");
}
}
private static void F()
{
Console.WriteLine("[线程F] 开启...");
Thread.Sleep(350);
Console.WriteLine("[线程F] [就绪队列]");
lock (objLockTest)
{
Console.WriteLine("[线程F] [持有锁的线程]" + DateTime.Now.ToString("HH:mm:ss:fff"));
Thread.Sleep(500);
Console.WriteLine("[线程F] 退出...");
}
}
软件运行具体结果及对照如下:
从上面看Monitor.PulseAll为将[等待队列]所有的线程移到[就绪队列],
[就绪队列]中排序为:原来排在最前的线程E,然后是以主线程开启子线程的顺序(start的顺序)排序。
问:若是一个Monitor.Pulse呢?
答:线程Pulse后将[等待队列] 最前的线程 移到[就绪队列], 即默认将第一个调用Wait的线程移动到[就绪队列]
部分代码及截图如下:
问:若是两个Monitor.Pulse呢?
答:分别两次将[等待队列] 最前的线程 移到[就绪队列],
[就绪队列]中排序为:原来排在最前的线程E,然后是这两个子线程开启的顺序(start的顺序)排序。
部分代码及截图如下:
问:若是三个Monitor.Pulse呢?
答:分别三次将[等待队列] 最前的线程 移到[就绪队列],
[就绪队列]中排序为:原来排在最前的线程E,然后是这两个子线程开启的顺序(start的顺序)排序。
部分代码及截图如下:
结论:
只有一个线程,则此线程为Wait超时线程或[等待队列]的首线程。
若只有一个Monitor.Pulse,则转入[就绪队列]的线程为第一个Wait的线程。
若 有多个Monitor.Pulse,则转入[就绪队列]的线程的顺序为主线程开启子线程的顺序(start的顺序)。
从上面程序及运行结果得出如下结论:
<问1>若有多个wait同时起作用,那么多个线程状态在[等待队列]中的排序是?
<答1>多个wait起作用后多个线程依次进入[等待队列]状态,即最先进入[等待队列]的排在最前。
<问2> 单个Pulse作用是将[等待队列] 最前的线程 移到[就绪队列]的第几位?
<答2>单个Pulse运行后从本程序上看,应该是[等待队列] 最前的线程 大概率插队到[就绪队列]的第二位(如上软件运行结果及对应释义图)
个人增加问题及答案:
<问 3>wait的超时时间及返回值的作用是?
<答 3>超时时间为当前线程从[等待队列]到进入[就绪队列]的时间差,
超时时间作用如下:
a.超时时间为0,则本线程直接进入[就绪队列]
b.若已超时,则本线程会自动进入[就绪队列]。
返回值的作用如下:
a.本线程重新获取该锁后,若在指定的时间过期之前从[等待队列]进入[就绪队列],返回值为True,
b.本线程重新获取该锁后,若在指定的时间过期之后从[等待队列]进入[就绪队列],返回值为False。
<问 4>线程如何从[等待队列]进入[就绪队列] ?
<答 4>两种方式
a.第一种wait设定超时时间,超时后自动进入[就绪队列] (如上软件运行结果及对应释义图)
b.第二种其他线程运行Monitor.Pulse(同参数),将 [等待队列]最前的线程移动到[就绪队列]
<问 5>正常情况下Wait与Pulse的调用顺序是?分别什么影响?
<答 5>理论上应该是 某个线程先调用Monitor.Wait,然后另一线程再调用Monitor.Pulse,具体使用方法参考上面的代码。
若[等待队列]中无线程,另一线程先调用Monitor.Pulse,某个线程再调用Monitor.Wait,则Monitor.Pulse无作用(如上软件运行结果及对应释义图中线程A),
且后面调用的Monitor.Wait可能会导致死锁(参考链接微软官方文档[4])。为防止死锁,则建议在调用Monitor.Wait时设置足够的超时时间。
<问 6>请问Monitor.PulseAll具体的作用是?
<答 6>作用为将[等待队列]的所有线程移到[就绪队列]中,并放到[就绪队列]的第二位。
若有多个线程,则以主线程开启子线程的顺序(start的顺序)排序。
[等待队列]中的排队顺序为:调用Wait的顺序依次排列
[就绪队列]中的排队顺序为:队伍首线程,然后是“Wait超时,或调用后Pulse/PulseAll的线程”,再是其他线程。
若“Wait超时,或调用后Pulse/PulseAll的线程”
只有一个线程,则此线程为Wait超时线程或[等待队列]的首线程。
若有多个线程则排列顺序为:主线程开启子线程的顺序(start的顺序)。
关于Wait和Pulse的问与答:
<问>线程Wait后如何回到[持有锁的线程] ?
<答>当前线程运行到wait后进入了[等待队列],而本线程若想重新进入[持有锁的线程]则必须先转为[就绪队列]状态并排堆,只有进入[就绪队列]且排队后才有可能转为[持有锁的线程]。
<问>若wait需要设超时时间吗?
<答>我们都知道线程运行到wait后则进行到[等待队列],需要通过Monitor.Pulse(同参数)或超时时间进入[就绪队列],否则本线程就会一直处于[等待队列]。
<问>Monitor.Pulse会将线程移到[就绪队列]的哪里?
<答> 个人认为会直接移动到[就绪队列]的第二位,即当前状态为(排队中)的第二个
Monitor.Pulse后会将同参数中处于[等待队列] 最前的线程移动[就绪队列],
而Monitor.PulseAll会将同参数中处于[等待队列] 所有线程移到[就绪队列]
编译环境:Visual Studio 2012
编辑框架:.NET Framework 4.0
编程语言:C#
源代码链接:https://download.csdn.net/download/shengmingzaiyuxuexi/12327588
参考链接
[1]:https://docs.microsoft.com/zh-cn/dotnet/standard/threading/managed-threading-best-practices
[2]:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.monitor?view=netframework-4.8
[3]:https://docs.microsoft.com/en-us/dotnet/api/system.threading.monitor?view=netframework-4.8
[4]:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.monitor.pulse?view=netframework-4.8
[5]:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.monitor.wait?view=netframework-4.8
有错误,请指正,有疑问,请留言,与君共勉,共同进步!
谢老板的观,看您的肯定就是我创作的最大动力,下期见!