C#:监控 - 等待,脉冲,PulseAll

时间:2009-10-13 10:08:41

标签: c# multithreading

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

7 个答案:

答案 0 :(得分:52)

简短版本:

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只唤醒一个线程 - 但我不够聪明,不能证明下一个线程总是我想要的,所以我倾向于使用{{1}在继续之前简单地重新验证条件;举个例子:

PulseAll

通过这种方法,我可以安全地添加 while (queue.Count >= maxSize) { Monitor.Wait(queue); } 的其他含义,而我现有的代码假设“我醒了,因此有数据” - 这在以后需要的时候(在同一个例子中)很方便添加Pulse方法。

答案 1 :(得分:39)

使用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!");
}

答案 2 :(得分:9)

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

答案 3 :(得分:1)

它们是线程之间同步和信令的工具。因此,它们不会阻止死锁,但如果使用正确,它们可用于在线程之间进行同步和通信。

不幸的是,编写正确的多线程代码所需的大部分工作目前是开发人员在C#(以及许多其他语言)中的责任。看看F#,Haskell和Clojure如何处理这种完全不同的方法。

答案 4 :(得分:1)

不幸的是,Wait(),Pulse()或PulseAll()的 none 具有您希望的神奇属性 - 这就是使用 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();
}

这段代码会死锁!也许不是今天,也许不是明天。最有可能的情况是,当您的代码处于压力之下时,因为它突然变得流行或重要,并且您被要求解决紧急问题。

为什么?

最终会发生以下情况:

  1. 所有消费者线程都在做一些工作
  2. 消息到达,就绪区域不能再保留任何消息,并且会调用PulseAll()。
  3. 没有消费者被唤醒,因为没有人等待
  4. 所有消费者线程都调用Wait()[DEADLOCK]
  5. 这个特殊的例子假设生产者线程永远不会再次调用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();
    }
    

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

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

    2. 该线程获取锁

      由于锁定,现在

    3. 一个。消息尚未到达就绪区域,它通过调用Wait()释放锁定,然后消息接收器线程可以获取锁定并将更多消息复制到就绪区域,

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

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

      现在请注意,在此代码中,必须完全正确地使用锁定范围。 (如果,我确实做对了!)

      此外,由于您必须使用lock(或Monitor.Enter()等)才能以无死锁的方式使用Monitor.PulseAll()Monitor.Wait(),不得不担心由于锁定而发生其他死锁的可能性。

      底线:这些API也容易搞砸和死锁,即非常危险

答案 5 :(得分:1)

总共把我扔到这里的东西是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)的锁定。只有赢得锁定的人才能继续。

答案 6 :(得分:0)

这是显示器使用的简单示例:

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();
        }
    }
}