【发布时间】:2010-12-06 06:26:42
【问题描述】:
我很难理解Wait()、Pulse()、PulseAll()。他们都会避免僵局吗?如果您解释如何使用它们,我将不胜感激?
【问题讨论】:
标签: c# multithreading
我很难理解Wait()、Pulse()、PulseAll()。他们都会避免僵局吗?如果您解释如何使用它们,我将不胜感激?
【问题讨论】:
标签: c# multithreading
短版:
lock(obj) {...}
是Monitor.Enter / Monitor.Exit 的简写(带有异常处理等)。如果没有其他人拥有锁,您可以获得它(并运行您的代码) - 否则您的线程将被阻塞,直到获得锁(由另一个线程释放它)。
死锁通常发生在 A:两个线程以不同的顺序锁定事物时:
thread 1: lock(objA) { lock (objB) { ... } }
thread 2: lock(objB) { lock (objA) { ... } }
(这里,如果他们每个人都获得了第一个锁,那么永远都无法获得第二个锁,因为两个线程都不能退出以释放他们的锁)
这种情况可以通过始终以相同的顺序锁定来最小化;并且您可以通过使用Monitor.TryEnter(而不是Monitor.Enter/lock)并指定超时来恢复(在一定程度上)。
或B:你可以在持有锁的同时线程切换时用winforms之类的东西来阻止自己:
lock(obj) { // on worker
this.Invoke((MethodInvoker) delegate { // switch to UI
lock(obj) { // oopsiee!
...
}
});
}
上面的死锁看起来很明显,但是当你有意大利面条代码时就不那么明显了;可能的答案:不要在持有锁时进行线程切换,或者使用BeginInvoke 这样您至少可以退出锁(让 UI 播放)。
Wait/Pulse/PulseAll不同;它们是用来发信号的。我用这个in this answer 发出信号,以便:
Dequeue:如果在队列为空的情况下尝试出列数据,它会等待另一个线程添加数据,从而唤醒阻塞的线程Enqueue:如果你在队列满的时候尝试入队数据,它会等待另一个线程移除数据,这会唤醒阻塞的线程Pulse 只唤醒 一个 线程 - 但我没有足够的头脑来证明下一个线程总是我想要的,所以我倾向于使用PulseAll,并且简单在继续之前重新验证条件;例如:
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
【讨论】:
让我印象深刻的是Pulse 只是给Wait 中的线程一个“提示”。在执行Pulse 的线程放弃锁并且等待线程成功赢得它之前,等待线程将不会继续。
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) 的锁。只有赢得锁定的人才能继续。
【讨论】:
它们是用于在线程之间进行同步和发送信号的工具。因此,它们对防止死锁没有任何作用,但如果使用得当,它们可以用于线程之间的同步和通信。
不幸的是,编写正确的多线程代码所需的大部分工作目前都由 C#(和许多其他语言)的开发人员负责。看看 F#、Haskell 和 Clojure 如何以完全不同的方法处理这个问题。
【讨论】:
不幸的是,Wait()、Pulse() 或 PulseAll() 的 none 具有您所希望的神奇属性 - 即通过使用 this 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();
}
这段代码会死锁!也许不是今天,也许不是明天。最有可能是当您的代码因为突然变得流行或重要而受到压力时,您被要求解决紧急问题。
为什么?
最终会发生以下情况:
这个特定的例子假设生产者线程永远不会再调用 PulseAll(),因为它没有更多的空间来放置消息。但是这个代码可能有很多很多损坏的变体。人们会尝试通过更改诸如将Monitor.Wait(); 变为
if (!canGrabMessage()) Monitor.Wait(incomingMessages);
不幸的是,这仍然不足以修复它。要修复它,您还需要更改调用 Monitor.PulseAll() 的锁定范围:
LoopOnMessages()
{
lock(incomingMessages)
{
if (!canGrabMessage()) Monitor.Wait(incomingMessages);
}
if (canGrabMessage()) handleMessage();
// loop
}
ReceiveMessagesAndSignalWaiters()
{
awaitMessagesArrive();
lock(incomingMessages)
{
copyMessagesToReadyArea();
Monitor.PulseAll(incomingMessages); //or Monitor.Pulse
}
awaitReadyAreaHasFreeSpace();
}
关键是在固定代码中,锁限制了可能的事件序列:
消费者线程完成其工作并循环
那个线程获得了锁
并且由于锁定,现在 或者:
一个。消息还没有到达就绪区,它通过调用 Wait() 释放锁,然后消息接收线程可以获取锁并将更多消息复制到就绪区,或
b.消息已经到达在就绪区域,它接收消息而不是调用 Wait()。 (当它做出这个决定时,消息接收者线程不可能例如获取锁并将更多消息复制到就绪区域。)
因此,原始代码的问题现在永远不会发生: 3. 调用 PulseEvent() 时 没有消费者被唤醒,因为没有人在等待
现在请注意,在此代码中,您必须完全正确地获取锁定范围。 (如果我确实做对了!)
而且,由于您必须使用lock(或Monitor.Enter() 等)才能以无死锁的方式使用Monitor.PulseAll() 或Monitor.Wait(),您仍然需要担心其他由于该锁定而发生的死锁。
底线:这些 API 也很容易搞砸和死锁,即非常危险
【讨论】:
这是一个简单的监视器使用示例:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp4
{
class Program
{
public static int[] X = new int[30];
static readonly object _object = new object();
public static int count=0;
public static void PutNumbers(int numbersS, int numbersE)
{
for (int i = numbersS; i < numbersE; i++)
{
Monitor.Enter(_object);
try
{
if(count<30)
{
X[count] = i;
count++;
Console.WriteLine("Punt in " + count + "nd: "+i);
Monitor.Pulse(_object);
}
else
{
Monitor.Wait(_object);
}
}
finally
{
Monitor.Exit(_object);
}
}
}
public static void RemoveNumbers(int numbersS)
{
for (int i = 0; i < numbersS; i++)
{
Monitor.Enter(_object);
try
{
if (count > 0)
{
X[count] = 0;
int x = count;
count--;
Console.WriteLine("Removed " + x + " element");
Monitor.Pulse(_object);
}
else
{
Monitor.Wait(_object);
}
}
finally
{
Monitor.Exit(_object);
}
}
}
static void Main(string[] args)
{
Thread W1 = new Thread(() => PutNumbers(10,50));
Thread W2 = new Thread(() => PutNumbers(1, 10));
Thread R1 = new Thread(() => RemoveNumbers(30));
Thread R2 = new Thread(() => RemoveNumbers(20));
W1.Start();
R1.Start();
W2.Start();
R2.Start();
W1.Join();
R1.Join();
W2.Join();
R2.Join();
}
}
}
【讨论】: