【问题标题】:C# : Monitor - Wait,Pulse,PulseAllC#:监视器 - 等待、脉冲、脉冲全部
【发布时间】:2010-12-06 06:26:42
【问题描述】:

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

【问题讨论】:

    标签: c# multithreading


    【解决方案1】:

    短版:

    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() 方法。

    【讨论】:

      【解决方案2】:

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

      【讨论】:

      • 我不明白。老板和工人怎么可能同时处于“电话”锁定状态?
      • @Marshall Monitor.Wait 将“电话”锁定释放到下一个排队者(可能是老板)。
      • @DennisGorelik 啊,我明白了。我已经扩展了你的观点below
      • 感谢您的类比 - 这真的很有帮助。
      • 你的第一个例子可能会死锁。假设老板首先获得了锁,向所有等待的线程(此时没有人)发送脉冲,然后释放锁并等待重新获取(Monitor.Wait)。一个工人抓住锁,然后释放它,等待永远不会到来的脉冲。当工人释放这个锁时,注意老板不能重新获取它,因为没有发送脉冲。对于任何获得锁的人来说,他们必须在就绪队列中。要从等待队列到就绪队列,需要发送一个脉冲。
      【解决方案3】:

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

      【讨论】:

      • 对于新手强烈推荐 albahari 页面是完美的!通过清晰的示例逐步完成线程和同步。
      【解决方案4】:

      让我印象深刻的是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) 的锁。只有赢得锁定的人才能继续。

      【讨论】:

        【解决方案5】:

        它们是用于在线程之间进行同步和发送信号的工具。因此,它们对防止死锁没有任何作用,但如果使用得当,它们可以用于线程之间的同步和通信。

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

        【讨论】:

          【解决方案6】:

          不幸的是,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();
          }
          

          这段代码会死锁!也许不是今天,也许不是明天。最有可能是当您的代码因为突然变得流行或重要而受到压力时,您被要求解决紧急问题。

          为什么?

          最终会发生以下情况:

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

          这个特定的例子假设生产者线程永远不会再调用 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() 释放锁,然后消息接收线程可以获取锁并将更多消息复制到就绪区,

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

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

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

          而且,由于您必须使用lock(或Monitor.Enter() 等)才能以无死锁的方式使用Monitor.PulseAll()Monitor.Wait(),您仍然需要担心其他由于该锁定而发生的死锁。

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

          【讨论】:

            【解决方案7】:

            这是一个简单的监视器使用示例:

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

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2022-11-10
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2012-08-26
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多