什么是Monitor.Pulse和Monitor.Wait的优势?

时间:2011-07-04 20:16:46

标签: c# multithreading concurrency

我对并发编程不熟悉,并试图了解使用Monitor.Pulse和Monitor.Wait的好处。

MSDN的示例如下:

class MonitorSample
{
    const int MAX_LOOP_TIME = 1000;
    Queue   m_smplQueue;

    public MonitorSample()
    {
        m_smplQueue = new Queue(); 
    }
    public void FirstThread()
    {
        int counter = 0;
        lock(m_smplQueue)
        {
            while(counter < MAX_LOOP_TIME)
            {
                //Wait, if the queue is busy.
                Monitor.Wait(m_smplQueue);
                //Push one element.
                m_smplQueue.Enqueue(counter);
                //Release the waiting thread.
                Monitor.Pulse(m_smplQueue); 

                counter++;
            }
        }
    }
    public void SecondThread()
    {
        lock(m_smplQueue)
        {
            //Release the waiting thread.
            Monitor.Pulse(m_smplQueue);
            //Wait in the loop, while the queue is busy.
            //Exit on the time-out when the first thread stops. 
            while(Monitor.Wait(m_smplQueue,1000))
            {
                //Pop the first element.
                int counter = (int)m_smplQueue.Dequeue();
                //Print the first element.
                Console.WriteLine(counter.ToString());
                //Release the waiting thread.
                Monitor.Pulse(m_smplQueue);
            }
        }
    }
    //Return the number of queue elements.
    public int GetQueueCount()
    {
        return m_smplQueue.Count;
    }

    static void Main(string[] args)
    {
        //Create the MonitorSample object.
        MonitorSample test = new MonitorSample();           
        //Create the first thread.
        Thread tFirst = new Thread(new ThreadStart(test.FirstThread));
        //Create the second thread.
        Thread tSecond = new Thread(new ThreadStart(test.SecondThread));
        //Start threads.
        tFirst.Start();
        tSecond.Start();
        //wait to the end of the two threads
        tFirst.Join();
        tSecond.Join();         
        //Print the number of queue elements.
        Console.WriteLine("Queue Count = " + test.GetQueueCount().ToString());
    }
}

我无法看到使用Wait And Pulse而不是这样做的好处:

    public void FirstThreadTwo()
    {
        int counter = 0;
        while (counter < MAX_LOOP_TIME)
        {
            lock (m_smplQueue)
            {
                m_smplQueue.Enqueue(counter);
                counter++;
            }
        }
    }
    public void SecondThreadTwo()
    {
        while (true)
        {
            lock (m_smplQueue)
            {
                    int counter = (int)m_smplQueue.Dequeue();
                    Console.WriteLine(counter.ToString());
            }
        }
    }

非常感谢任何帮助。 谢谢

4 个答案:

答案 0 :(得分:12)

为了描述“优势”,关键问题是“什么?”。如果你的意思是“优先考虑热循环”,那么CPU利用率是显而易见的。如果你的意思是“优先于睡眠/重试循环” - 你可以获得更快的响应(Pulse不需要等待那么久)使用更低的CPU(你没有'不必要地吵醒了2000次。

但一般来说,人们的意思是“优先考虑Mutex等”。

我倾向于广泛使用这些 ,即使优先考虑互斥,重置事件等;原因:

  • 它们很简单,涵盖了我需要的大部分场景
  • 它们相对便宜,因为它们不需要一直到操作系统句柄(不像操作系统拥有的Mutex等)
  • 我一般已经使用lock来处理同步,所以当我需要等待某事时,我已经有lock的机会很好
  • 它实现了我的正常目标 - 允许2个线程以管理的方式向彼此发出完成信号
  • 我很少需要Mutex等的其他功能(例如进程间)

答案 1 :(得分:4)

你的代码片段存在严重缺陷,当SecondThreadTwo()尝试在空队列上调用Dequeue()时,它会严重失败。你可能通过让FirstThreadTwo()在消费者线程之前执行一小段时间来完成它,可能首先启动它。这是一个意外,一个在运行这些线程一段时间后停止工作或者用不同的机器负载启动它们的事故。这可能会在一段时间内意外地无错误地工作,很难诊断偶尔的故障。

没有办法编写一个锁定算法来阻止使用者,直到队列变为非空而只有lock语句。一个不断进入和退出锁定的繁忙循环有效,但却是一个非常糟糕的替代品。

编写这种代码最好留给线程专家,很难证明它在所有情况下都能正常工作。不仅没有像这样的失败模式或线程竞赛。但也算法的一般适应性,避免死锁,活锁和线程车队。在.NET世界中,大师是Jeffrey Richter和Joe Duffy。他们在书籍,博客和杂志文章中都吃早餐的锁定设计。窃取他们的代码是预期和接受的。并部分进入.NET框架,并添加了System.Collections.Concurrent命名空间。

答案 2 :(得分:3)

正如您所猜测的那样,使用Monitor.Pulse / Wait是一项性能提升。获得锁是一种相对昂贵的操作。通过使用Monitor.Wait,你的线程将一直处于休眠状态,直到某个其他线程用`Monitor.Pulse'唤醒你的线程。

您将看到TaskManager的不同之处,因为即使队列中没有任何内容,也会挂起一个处理器核心。

答案 3 :(得分:0)

PulseWait的优点是它们可以用作所有其他同步机制的构建块,包括互斥体,事件,障碍等。有些事情可以通过{{{ 1}}和Pulse无法通过BCL中的任何其他同步机制完成。

所有有趣的东西都发生在Wait方法中。 Wait将退出临界区,并通过将其置于等待队列中将线程置于Wait状态。调用WaitSleepJoin后,等待队列中的下一个线程将移至就绪队列。一旦线程切换到Pulse状态,它就会重新进入临界区。重复另一种方式很重要。 Running将释放锁定并以原子方式重新获取 。没有其他同步机制具有此功能。

设想这种方法的最佳方法是尝试使用其他策略复制行为,然后查看可能出现的问题。让我们使用Wait来尝试此问题,因为ManualResetEventSet方法似乎类似于它们。我们的第一次尝试可能是这样的。

WaitOne

应该很容易看到代码会死锁。那么如果我们尝试这种天真的修复会发生什么呢?

void FirstThread()
{
  lock (mre)
  {
    // Do stuff.
    mre.Set();
    // Do stuff.
  }
}

void SecondThread()
{
  lock (mre)
  {
    // Do stuff.
    while (!CheckSomeCondition())
    {
      mre.WaitOne();
    }
    // Do stuff.
  }
}

你能看到这里出了什么问题吗?由于在检查等待条件后我们没有原子地重新进入锁定,因此另一个线程可以进入并使条件无效。换句话说,另一个线程可以执行某些操作,导致void FirstThread() { lock (mre) { // Do stuff. mre.Set(); // Do stuff. } } void SecondThread() { lock (mre) { // Do stuff. } while (!CheckSomeCondition()) { mre.WaitOne(); } lock (mre) { // Do stuff. } } 在重新获取以下锁之前再次开始返回CheckSomeCondition。如果您的第二个代码块要求条件为false,那肯定会导致许多奇怪的问题。