为什么Monitor.Pulse需要锁定互斥锁? (。净)

时间:2009-12-24 13:20:36

标签: .net mutex monitor pulse

Monitor.Pulse和PulseAll要求在操作时锁定它的锁定。这个要求似乎是不必要的,对性能有害。我的第一个想法是,这导致2个浪费的上下文切换,但这由下面的nobugz更正(谢谢)。我仍然不确定它是否涉及浪费的上下文切换的潜在,因为在监视器上等待的其他线程已经可用于sheduler,但如果他们被安排,他们将只能够在点击互斥锁之前运行一些指令,并且必须再次进行上下文切换。如果在调用Monitor.Pulse之前解锁,这看起来会更简单,更快。

Pthread condition variables实现相同的概念,但它没有上述限制:即使您不拥有互斥锁,也可以调用pthread_cond_broadcast。我认为这证明了要求是不合理的。

修改: 我意识到需要一个锁来保护通常在Monitor.Pulse之前更改的共享资源。我试图说这个锁可以在访问资源之后但在Pulse之前解锁,因为Monitor会支持这个。这将有助于将锁限制为访问共享资源的最短时间。就这样:

void f(Item i)
{
  lock(somequeue) {
    somequeue.add(i);
  }
  Monitor.Pulse(somequeue);  // error
}

4 个答案:

答案 0 :(得分:2)

原因与内存障碍和线程安全有关。

所有涉及的线程都将检查用于确定是否需要Pulse()的共享变量(条件)。如果没有内存屏障,更改可能会保留在寄存器中,并且从一个线程到另一个线程不可见。在跨线程查看时,也可以重新排序读取和写入。

但是,从锁中访问的变量使用内存屏障,因此所有相关线程都可以访问它们。锁中的所有操作似乎都是从持有相同锁的其他线程的角度原子执行的。

此外,您不需要多个上下文切换,如您所假设的那样。等待线程被放入一个(标称FIFO)队列,当它们被Pulse()触发时,它们不能完全运行,直到放弃锁定(再次,部分是由于内存障碍)。

有关问题的详细讨论,请参阅:http://www.albahari.com/threading/part4.aspx#_Wait_and_Pulse

答案 1 :(得分:2)

您对Pulse()调用调用线程切换的假设不正确。它只是将一个线程从等待队列移动到就绪队列。 Exit()调用将切换到就绪队列中的第一个线程。

答案 2 :(得分:1)

我在本文中找到了答案:

http://research.microsoft.com/pubs/64242/implementingcvs.pdf

它声明:

  

由于我们正在考虑这个级别的线程实现,我   应该指出最后一个性能问题,以及该怎么做   它。如果在保持锁定m的情况下调用Signal,并且正在运行   在多处理器上,新唤醒的线程很可能   立即开始运行。这将导致它再次阻止一些   稍后在(2)当它想要锁定m时的指令。如果你想   避免这些额外的重新安排,你需要安排转移   线程直接从条件变量队列到队列   等待m的线程。这在Java或C#中尤其重要,   两者都要求在呼叫信号或广播时保持m。

这篇论文的内容有点模糊,并没有提到很多实施细节,而是伪/学术层面。但显然,编写它的人在实际的.net实现中负有责任。

但粗略地说:信号只是一个逻辑/用户级别的操作,并且不会立即触发像条件变量信号这样的原语。它只在锁定范围出口处这样做。所以没有性能问题。当一个人用来直接操纵条件变量时,确实令人不安。

答案 3 :(得分:0)

等待旨在与条件检查一起使用。如果未在锁内完成条件检查,则可能发生以下事件序列:

  1. 条件检查表示需要等待。
  2. 另一个线程会更改条件,因此不需要等待,然后执行Pulse或PulseAll。
  3. 第一个线程,观察到需要等待,执行等待。

一旦发生了这一系列事件,完全有可能再没有任何东西可以再次激活锁定(除非出现再次需要等待的情况,并且再次不再需要)。因此,线程#1可以永远等待永远不会到来的事件。

在锁定中进行条件检查和等待避免了这种危险,因为在检查条件的时间和等待开始的时间之间,另一个线程无法改变条件。因此,另一个更改条件并执行脉冲的线程可以确保第一个线程在更改后检查条件(从而避免等待)或者在执行脉冲时执行等待(因此能够恢复) )。