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】:

您需要了解PulsePulseAllWait 在做什么。 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 在设计上有缺陷吗?不应该只有两个队列;应该有多个等待队列,每个队列都针对由 对指定的特定等待条件,就像 pthreads 所做的那样。一个大的等待队列会导致过多的上下文切换,当一个线程醒来时发现等待条件根本没有改变,然后立即回到等待队列。 @h9uest:你说得很好。我不知道我是否会说这是一个缺陷,但目前的设计肯定有缺点。会不会更好?可能......是的,但代价是什么?【参考方案2】:

你必须不断检查队列是否仍然是空的。仅使用 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,条件变量的主要内容,如果未能解决你的问题,请参考以下文章

线程同步——条件变量

线程同步——条件变量

再谈条件变量—从入门到出家

DEA模型变量要满足的条件

POSIX条件变量

[C++11 多线程同步] --- 条件变量的那些坑条件变量信号丢失和条件变量虚假唤醒(spurious wakeup)