给出以下代码片段(在学习线程时在某处找到)。
public class BlockingQueue<T>
{
private readonly object sync = new object();
private readonly Queue<T> queue;
public BlockingQueue()
{
queue = new Queue<T>();
}
public void Enqueue(T item)
{
lock (sync)
{
queue.Enqueue(item);
Monitor.PulseAll(sync);
}
}
public T Dequeue()
{
lock (sync)
{
while (queue.Count == 0)
Monitor.Wait(sync);
return queue.Dequeue();
}
}
}
我想要了解的是,
为什么会有一个while循环?
while (queue.Count == 0)
Monitor.Wait(sync);
以及
的错误 if(queue.Count == 0)
Monitor.Wait(sync);
事实上,当我看到我使用while循环找到的类似代码的时候,任何人都可以帮助我理解一个在另一个之上的使用。 谢谢。
答案 0 :(得分:18)
您需要了解Pulse
,PulseAll
和Wait
正在做什么。 Monitor
维护两个队列:等待队列和就绪队列。当线程调用Wait
时,它将被移入等待队列。当一个线程调用Pulse
时,它会将一个且只有一个线程从等待队列移动到就绪队列。当线程调用PulseAll
时,它将所有线程从等待队列移动到就绪队列。就绪队列中的线程可以随时重新获取锁定,但只有在当前持有者释放它之后才会重新获取锁定。
基于这些知识,很容易理解为什么在使用PulseAll
时必须重新检查队列数。这是因为所有出列线程最终都会被唤醒并且想要尝试从队列中提取项目。但是,如果队列中只有一个项目开始,该怎么办?显然,我们必须重新检查队列计数以避免将空队列出列。
那么,如果您使用Pulse
代替PulseAll
,结论会是什么?简单的if
检查仍然存在问题。原因是因为来自就绪队列的线程不一定是下一个获取锁的线程。这是因为Monitor
不会优先于Wait
来电之前的Enter
来电。
使用while
时,Monitor.Wait
循环是一种相当标准的模式。这是因为脉冲线程本身没有语义含义。这只是锁定状态发生变化的信号。当线程在阻塞Wait
后唤醒时,它们应该重新检查最初用于阻塞线程的相同条件,以查看该线程现在是否可以继续。有时它不能,所以它应该阻止更多。
这里最好的经验法则是,如果对是否使用if
检查或while
检查存在疑问,请始终选择while
循环,因为它更安全。事实上,我会把这种情况发挥到极致,并建议总是使用while
循环,因为使用更简单的if
检查没有固有的优势,因为{{无论如何,检查几乎总是错误的选择。类似的规则适用于选择是使用if
还是Pulse
。如果对使用哪一个产生疑问,请始终选择PulseAll
。
答案 1 :(得分:3)
你必须继续检查队列是否仍然是空的。仅使用if只检查一次,等待一段时间,然后出列。如果那时队列仍然是空的怎么办?砰!队列下溢错误...
答案 2 :(得分:1)
if 条件当某些内容发布时, queue.Count == 0 将不会再次检查并且可能是队列下溢错误,因此我们必须每次都检查一下条件时间因为并发而被称为旋转
答案 3 :(得分:0)
为什么在Unix上它可能出错是因为虚假唤醒,可能是由OS信号引起的。这是一个副作用,不能保证永远不会发生在Windows上。这不是遗产,它是操作系统的工作方式。如果监视器是根据条件变量实现的,那就是。
def:虚假唤醒是条件变量等待站点上的休眠线程的重新调度,不是来自当前程序线程的动作触发的(如{{1 }})。
这种不便可以通过例如管理语言来掩盖。队列。因此,在退出Pulse()
函数之前,框架可以检查这个正在运行的线程实际上是否真的被请求进行调度,如果它没有在运行队列中找到它自己可以返回休眠状态。隐藏问题。
答案 4 :(得分:-1)
if (queue.Count == 0)
会做的。
我认为,使用while循环模式进行“等待和检查条件”上下文是遗留下来的遗留问题。因为非Windows,非.NET监视器变量可以在没有实际Pulse
的情况下触发。
在.NET中,如果没有Queue
填充,则无法触发私有监视器变量,因此您无需担心监视器等待后的队列下溢。但是,使用while循环“等待和检查条件”真的不是坏习惯。