什么是监视器.脉冲和监视器.等待优势
本文关键字:监视器 等待 脉冲 什么 | 更新日期: 2023-09-27 18:01:13
我对并发编程有点陌生,并试图理解使用Monitor.Pulse和Monitor.Wait.的好处
MSDN的示例如下:
class MonitorSample
{
const int MAX_LOOP_TIME = 1000;
Queue m_smplQueue;
public MonitorSample()
{
m_smplQueue = new Queue();
}
public void FirstThread()
{
int counter = 0;
lock(m_smplQueue)
{
while(counter < MAX_LOOP_TIME)
{
//Wait, if the queue is busy.
Monitor.Wait(m_smplQueue);
//Push one element.
m_smplQueue.Enqueue(counter);
//Release the waiting thread.
Monitor.Pulse(m_smplQueue);
counter++;
}
}
}
public void SecondThread()
{
lock(m_smplQueue)
{
//Release the waiting thread.
Monitor.Pulse(m_smplQueue);
//Wait in the loop, while the queue is busy.
//Exit on the time-out when the first thread stops.
while(Monitor.Wait(m_smplQueue,1000))
{
//Pop the first element.
int counter = (int)m_smplQueue.Dequeue();
//Print the first element.
Console.WriteLine(counter.ToString());
//Release the waiting thread.
Monitor.Pulse(m_smplQueue);
}
}
}
//Return the number of queue elements.
public int GetQueueCount()
{
return m_smplQueue.Count;
}
static void Main(string[] args)
{
//Create the MonitorSample object.
MonitorSample test = new MonitorSample();
//Create the first thread.
Thread tFirst = new Thread(new ThreadStart(test.FirstThread));
//Create the second thread.
Thread tSecond = new Thread(new ThreadStart(test.SecondThread));
//Start threads.
tFirst.Start();
tSecond.Start();
//wait to the end of the two threads
tFirst.Join();
tSecond.Join();
//Print the number of queue elements.
Console.WriteLine("Queue Count = " + test.GetQueueCount().ToString());
}
}
我看不出使用等待和脉冲代替的好处
public void FirstThreadTwo()
{
int counter = 0;
while (counter < MAX_LOOP_TIME)
{
lock (m_smplQueue)
{
m_smplQueue.Enqueue(counter);
counter++;
}
}
}
public void SecondThreadTwo()
{
while (true)
{
lock (m_smplQueue)
{
int counter = (int)m_smplQueue.Dequeue();
Console.WriteLine(counter.ToString());
}
}
}
任何帮助都将不胜感激。感谢
要描述"优势",一个关键问题是"超过什么?"。如果你的意思是"首选热循环",那么CPU利用率是显而易见的。如果你的意思是"优先于睡眠/重试循环",你可以得到更快的响应(Pulse
不需要等待那么长时间)和使用更低的CPU(你没有不必要地醒来2000次)。
不过,一般来说,人们的意思是"偏爱羊肉等"。
我倾向于广泛使用这些,甚至优先于互斥、重置事件等;原因:
- 它们很简单,涵盖了我需要的大部分场景
- 它们相对便宜,因为它们不需要一直到操作系统句柄(与操作系统拥有的Mutex等不同)
- 我通常已经使用
lock
来处理同步,所以当我需要等待时,很可能我已经有了lock
- 它实现了我的正常目标——允许两个线程以托管的方式相互发送完成信号
- 我很少需要Mutex等的其他功能(例如跨进程)
您的代码片段中存在严重缺陷,当SecondThreadWo()尝试在空队列上调用Dequeue()时,它将严重失败。您可能是通过让FirstThreadWo()在使用者线程之前的几分之一秒执行,也可能是通过首先启动它来实现它。这是一个意外,在运行这些线程一段时间或用不同的机器负载启动它们后,它将停止工作。这可能会在相当长的一段时间内意外地无错误工作,很难诊断偶尔的故障。
没有办法编写一个锁定算法来阻止使用者,直到队列变成非空,只有lock语句。一个不断进出锁的繁忙循环可以工作,但它是一个非常糟糕的替代品。
编写这种代码最好留给线程大师,很难证明它在所有情况下都有效。不仅仅是缺少像这样的故障模式或线程竞争。但也适用于避免死锁、活锁和线程护送的算法。在.NET世界里,大师是Jeffrey Richter和Joe Duffy。他们早餐吃锁定设计,无论是在他们的书、博客还是杂志文章中。窃取他们的代码是意料之中的事,也是可以接受的。并在System.Collections.Concurrent命名空间中添加了部分内容,从而部分进入.NET框架。
正如您所猜测的,使用Monitor.Pulse/Wait是一种性能改进。获取锁是一项相对昂贵的操作。通过使用Monitor.Wait
,您的线程将休眠,直到其他线程用"Monitor.Pulse"唤醒您的线程。
您将在TaskManager中看到差异,因为即使队列中没有任何内容,也会固定一个处理器核心。
Pulse
和Wait
的优点是,它们可以用作所有其他同步机制的构建块,包括互斥、事件、屏障等。Pulse
和Wait
可以完成BCL中任何其他同步机制都无法完成的一些事情。
所有有趣的事情都发生在Wait
方法内部。Wait
将退出关键部分,并通过将线程放入等待队列将其置于WaitSleepJoin
状态。一旦调用了Pulse
,则等待队列中的下一个线程移动到就绪队列。一旦线程切换到Running
状态,它就会重新进入关键部分。用另一种方式重复这一点很重要。Wait
将释放锁并以原子方式重新获取它。没有其他同步机制具有此功能。
设想这种情况的最好方法是尝试用其他策略复制这种行为,然后看看会出什么问题。让我们用ManualResetEvent
来尝试这个例外,因为Set
和WaitOne
方法看起来可能是相似的。我们的第一次尝试可能是这样的。
void FirstThread()
{
lock (mre)
{
// Do stuff.
mre.Set();
// Do stuff.
}
}
void SecondThread()
{
lock (mre)
{
// Do stuff.
while (!CheckSomeCondition())
{
mre.WaitOne();
}
// Do stuff.
}
}
应该很容易看出代码可能会死锁。那么,如果我们尝试这种天真的解决方案,会发生什么呢?
void FirstThread()
{
lock (mre)
{
// Do stuff.
mre.Set();
// Do stuff.
}
}
void SecondThread()
{
lock (mre)
{
// Do stuff.
}
while (!CheckSomeCondition())
{
mre.WaitOne();
}
lock (mre)
{
// Do stuff.
}
}
你能看到这里出了什么问题吗?由于我们在检查等待条件后没有自动重新输入锁,因此另一个线程可能会进入并使条件无效。换句话说,另一个线程可以做一些事情,使CheckSomeCondition
在重新获取下面的锁之前再次开始返回false
。如果您的第二个代码块要求条件为true
,那么这肯定会导致很多奇怪的问题。