C#:监视器 – 等待,脉冲,PulseAll

我很难理解Wait()Pulse()PulseAll() 。 他们都会避免死锁吗? 如果你解释如何使用它们,我将不胜感激?

简洁版本:

 lock(obj) {...} 

Monitor.Enter / Monitor.Exit (具有exception处理等)的简短手段。 如果没有其他人有锁,你可以得到它(并运行你的代码) – 否则你的线程被阻塞,直到锁被获取(由另一个线程释放它)。

当A:两个线程以不同的顺序locking事件时,通常会发生死锁:

 thread 1: lock(objA) { lock (objB) { ... } } thread 2: lock(objB) { lock (objA) { ... } } 

(在这里,如果他们每个都获得第一个锁,那么既不能获得第二个锁,因为这两个线程都不能退出来释放他们的锁)

通过总是以相同的顺序locking,这种情况可以被最小化; 你可以通过使用Monitor.TryEnter (而不是Monitor.Enter / lock )并指定一个超时来恢复(某种程度上)。

或者B:在锁住线程切换时,你可以用winforms来阻止自己:

 lock(obj) { // on worker this.Invoke((MethodInvoker) delegate { // switch to UI lock(obj) { // oopsiee! ... } }); } 

这个死锁在上面显得很明显,但是当你有意大利面代码的时候并不是那么明显。 可能的答案:不要锁住线程,或者使用BeginInvoke这样至less可以退出locking(让UI玩游戏)。


Wait / Pulse / PulseAll不同; 他们是为了信号。 我在这个答案中使用这个信号,以便:

  • Dequeue队列:如果在队列为空时尝试出队数据,则等待另一个线程添加数据,这会唤醒被阻塞的线程
  • Enqueue :如果在队列满时尝试排队数据,它将等待另一个线程删除数据,这会唤醒阻塞的线程

Pulse只唤醒一个线程 – 但我不够聪明,以certificate下一个线程总是我想要的,所以我倾向于使用PulseAll ,只需重新validation条件,然后再继续; 举个例子:

  while (queue.Count >= maxSize) { Monitor.Wait(queue); } 

使用这种方法,我可以安全地添加Pulse其他含义,而不需要我现有的代码假设“我醒了,因此有数据” – 这在以后需要添加Close()方法时很方便。

简单的配方使用Monitor.Wait和Monitor.Pulse。 它由工人,老板和电话组成,他们用来沟通:

 object phone = new object(); 

“工人”线程:

 lock(phone) // Sort of "Turn the phone on while at work" { while(true) { Monitor.Wait(phone); // Wait for a signal from the boss DoWork(); Monitor.PulseAll(phone); // Signal boss we are done } } 

一个“老板”线程:

 PrepareWork(); lock(phone) // Grab the phone when I have something ready for the worker { Monitor.PulseAll(phone); // Signal worker there is work to do Monitor.Wait(phone); // Wait for the work to be done } 

更复杂的例子遵循…

一个“有别的事情的工人”:

 lock(phone) { while(true) { if(Monitor.Wait(phone,1000)) // Wait for one second at most { DoWork(); Monitor.PulseAll(phone); // Signal boss we are done } else DoSomethingElse(); } } 

一个“不耐烦的老板”:

 PrepareWork(); lock(phone) { Monitor.PulseAll(phone); // Signal worker there is work to do if(Monitor.Wait(phone,1000)) // Wait for one second at most Console.Writeline("Good work!"); } 

不,他们不保护你免于僵局。 它们只是线程同步的更灵活的工具。 这里有一个非常好的解释如何使用它们和非常重要的使用模式 – 没有这种模式,你会打破所有的东西: http : //www.albahari.com/threading/part4.aspx

阅读Jon Skeet的多部分线程文章 。

真的很棒。 你提到的那些大约有三分之一的方式。

它们是线程之间同步和信号传递的工具。 因此,它们不会阻止死锁,但是如果使用得当,它们可以用来在线程之间进行同步和通信。

不幸的是,编写正确的multithreading代码所需的大部分工作是C#(和许多其他语言)中开发人员的责任。 看看F#,Haskell和Clojure是如何处理这个问题的一个完全不同的方法。

不幸的是,Wait(),Pulse()或者PulseAll()都不具备你所希望的魔法属性 – 这就是通过使用这个 API你将自动避免死锁。

考虑下面的代码

 object incomingMessages = new object(); //signal object LoopOnMessages() { lock(incomingMessages) { Monitor.Wait(incomingMessages); } if (canGrabMessage()) handleMessage(); // loop } ReceiveMessagesAndSignalWaiters() { awaitMessages(); copyMessagesToReadyArea(); lock(incomingMessages) { Monitor.PulseAll(incomingMessages); //or Monitor.Pulse } awaitReadyAreaHasFreeSpace(); } 

这段代码会死锁! 也许不是今天,也许不是明天。 当你的代码被置于压力之下时,很可能是因为它突然变得stream行或重要,而且你正在被调用来解决紧急的问题。

为什么?

最终会发生以下情况:

  1. 所有消费者线程正在做一些工作
  2. 消息到达,就绪区域不能容纳更多消息,并调用PulseAll()。
  3. 没有消费者被唤醒,因为没有消费者在等待
  4. 所有消费者线程调用Wait()[DEADLOCK]

这个特殊的例子假设生产者线程永远不会再次调用PulseAll(),因为它没有更多的空间来放置消息。但是这个代码有很多很多的变化。 人们会尝试通过改变一行来使其更健壮,比如使Monitor.Wait();

 if (!canGrabMessage()) Monitor.Wait(incomingMessages); 

不幸的是,这还不足以解决这个问题。 为了解决这个问题,你需要改变Monitor.PulseAll()被调用的locking范围:

 LoopOnMessages() { lock(incomingMessages) { if (!canGrabMessage()) Monitor.Wait(incomingMessages); } if (canGrabMessage()) handleMessage(); // loop } ReceiveMessagesAndSignalWaiters() { awaitMessagesArrive(); lock(incomingMessages) { copyMessagesToReadyArea(); Monitor.PulseAll(incomingMessages); //or Monitor.Pulse } awaitReadyAreaHasFreeSpace(); } 

关键在于在固定代码中,锁限制了可能的事件序列:

  1. 消费者线程完成其工作并循环

  2. 那个线程获取锁

    而且感谢locking,现在是真的, 要么

  3. 一个。 消息尚未到达就绪区域,并通过在消息接收器线程可以获取locking并将更多消息复制到就绪区域之前调用Wait()来释放locking, 或者

    湾 消息已经到达就绪区域,它收到INSTEAD OF调用Wait()的消息。 (当它做出这个决定的时候,消息接收者线程不可能获得锁并将更多消息复制到就绪区域。)

结果,原始代码的问题现在不会发生:3.当调用PulseEvent()时, 没有消费者被唤醒,因为没有人在等待

现在观察在这个代码中,你必须得到locking范围完全正确 。 (如果的确,我说得对!)

此外,因为您必须使用lock (或Monitor.Enter()Monitor.Wait()以无死锁方式使用Monitor.PulseAll()Monitor.Wait() ,您仍然不必担心其他由于locking而发生的死锁。

底线:这些API也很容易搞砸和僵局,即相当危险

总是把我扔在这里的东西是Pulse只是给一个“ Wait ”的线程“抬头”。 Waiting线程将不会继续,直到执行Pulse的线程放弃locking ,并且等待的线程成功地获得locking

 lock(phone) // Grab the phone { Monitor.PulseAll(phone); // Signal worker Monitor.Wait(phone); // ****** The lock on phone has been given up! ****** } 

要么

 lock(phone) // Grab the phone when I have something ready for the worker { Monitor.PulseAll(phone); // Signal worker there is work to do DoMoreWork(); } // ****** The lock on phone has been given up! ****** 

在这两种情况下,直到“电话锁已经放弃”,另一个线程才能得到它。

可能有其他线程正在等待来自Monitor.Wait(phone)lock(phone) 。 只有赢得locking的才能继续。