1

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

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

编辑:我意识到需要一个锁来保护通常在 Monitor.Pulse 之前更改的共享资源。我想说的是,在访问资源之后但在 Pulse 之前,该锁可能已经解锁,因为 Monitor 会支持这一点。这将有助于将锁定限制在访问共享资源的最短时间。像这样:

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

4 回答 4

2

原因与内存屏障和保证线程安全有关。

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

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

此外,正如您所假设的,不需要多个上下文切换。等待线程被放入(名义上的 FIFO)队列中,当它们被 Pulse() 触发时,它们在释放锁之前不能完全运行(同样,部分原因是内存屏障)。

有关这些问题的良好讨论,请参阅:http ://www.albahari.com/threading/part4.aspx#_Wait_and_Pulse

于 2009-12-24T13:41:19.907 回答
2

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

于 2009-12-24T16:11:42.143 回答
1

我在这篇论文中找到了答案:

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

它指出:

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

这篇论文整体有点模糊,没有提到很多实现细节,而是处于伪/学术水平。但显然,编写它的人对实际的 .net 实施负有责任。

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

于 2013-02-21T09:48:20.640 回答
0

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

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

一旦发生了这一系列事件,完全有可能什么都不会再次脉冲锁(除非出现再次需要等待并且不再需要等待的情况)。因此,线程#1 可以永远等待一个永远不会到达的事件。

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

于 2011-01-21T20:21:08.403 回答