Monitor.Wait,条件变量
Posted
技术标签:
【中文标题】Monitor.Wait,条件变量【英文标题】:Monitor.Wait, Condition variable 【发布时间】:2011-09-13 17:05:26 【问题描述】:给定以下代码 sn-p(在学习线程时在某处找到)。
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循环发现的类似代码时,任何人都可以帮助我理解一个高于另一个的用法。 谢谢。
【问题讨论】:
您可能需要指定语言并添加适当的标签。 忘记了,已经有人加了C#标签。 这与 Hoare 语义与 Mesa Semantics [Mesa 调度] 有关。通常,您必须使用带有 Mesa 调度的循环。但在您的特殊情况下,这无关紧要。 【参考方案1】:您需要了解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
。如果对使用哪一个有疑问,请始终选择PulseAll
。
【讨论】:
+1 非常有用的监视器使用指南。事实上,没有双重锁定的 'if' 只不过是一场调试灾难。 @Brain Gideon。很棒的解释! 你不觉得 .NET Monitor 在设计上有缺陷吗?不应该只有两个队列;应该有多个等待队列,每个队列都针对由你必须不断检查队列是否仍然是空的。仅使用 if 只会检查一次,等待一段时间,然后出队。如果那个时候队列还是空的怎么办?砰!队列下溢错误...
【讨论】:
但是 Wait 只会在 Pulse 后唤醒,并且会在某些东西入队时发生。【参考方案3】:在 if 条件下,当某物释放锁时,queue.Count == 0 将不会再次检查,并且可能会出现队列下溢错误,因此我们必须每次检查条件时间因为并发,这被称为 Spinning
【讨论】:
【参考方案4】:为什么在 Unix 上它可能出错是因为虚假唤醒,可能是由操作系统信号引起的。这是一个副作用,也不能保证在 Windows 上也不会发生。这不是遗产,而是操作系统的工作方式。如果 Monitors 是按照 Condition Variable 来实现的,那就是。
def : spurious wake up 是在条件变量等待站点上重新调度休眠线程,它不是由来自当前程序线程的操作触发的(例如 Pulse()
)。
这种不便可以在托管语言中被掩盖,例如队列。因此,在退出Wait()
函数之前,框架可以检查这个正在运行的线程是否真的被请求调度,如果它没有发现自己在运行队列中,它可以重新进入睡眠状态。隐藏问题。
【讨论】:
【参考方案5】:if (queue.Count == 0)
会的。
我认为,对“等待和检查条件”上下文使用 while 循环模式是遗留问题。因为非Windows,非.NET的监控变量可以在没有实际Pulse
的情况下触发。
在 .NET 中,如果没有 Queue
填充,您的私有监视器变量将无法触发,因此您无需担心监视器等待后的队列下溢。但是,对于“等待和检查条件”使用while循环确实是一个不错的习惯。
【讨论】:
对我来说听起来很复杂,您能否指出一些我可以理解您的意思的资源。 好吧.. 恐怕我无法提供有关此的更详细的资源。这是我自己的经验。我在 Windows 中使用了“if ..”编码多年,没有任何问题。当我在 Linux 中使用“if ..”编码时?一切都搞砸了。您需要在 *Nix 平台中对监视器(条件)变量使用 while 循环。之后,无论语言/平台,我总是使用 while 循环。 恕我直言,一个简单的if
是行不通的。如果同时有另一个线程Dequeues
,您可能会遇到竞争情况。我怀疑这取决于是 PulseAll
还是 Pulse
被调用...
即使使用Pulse
,简单的if
检查仍然会出现问题。
由于之前的 lock(sync) 语句,多个线程无法执行 'if ...' 代码。以上是关于Monitor.Wait,条件变量的主要内容,如果未能解决你的问题,请参考以下文章
[C++11 多线程同步] --- 条件变量的那些坑条件变量信号丢失和条件变量虚假唤醒(spurious wakeup)