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行或重要,而且你正在被调用来解决紧急的问题。
为什么?
最终会发生以下情况:
- 所有消费者线程正在做一些工作
- 消息到达,就绪区域不能容纳更多消息,并调用PulseAll()。
- 没有消费者被唤醒,因为没有消费者在等待
- 所有消费者线程调用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(); }
关键在于在固定代码中,锁限制了可能的事件序列:
-
消费者线程完成其工作并循环
-
那个线程获取锁
而且感谢locking,现在是真的, 要么 :
-
一个。 消息尚未到达就绪区域,并通过在消息接收器线程可以获取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的才能继续。