Monitor.Pulse和Monitor.Wait有什么优势?

7

我是一个对并发编程比较新的人,正在尝试理解使用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个回答

12
为了描述“优势”,一个关键问题是“相比什么”。如果你的意思是“与热循环相比”,那么CPU利用率是显而易见的。如果你的意思是“与睡眠/重试循环相比” - 你可以得到更快的响应(Pulse不需要等待太长时间)并且使用更低的CPU(你不必不必无谓地唤醒2000次)。
通常,人们指的是“相较于Mutex等”。
我倾向于广泛使用这些,甚至优先于Mutex、reset-events等;原因如下:
  • 它们很简单,并且涵盖了我需要的大部分场景
  • 它们相对便宜,因为它们不需要到达操作系统句柄的全部程度 (与 Mutex 等不同,后者由操作系统拥有)
  • 当我需要等待某些事情发生时,我通常已经在使用lock来处理同步,所以很可能我已经有一个lock
  • 它实现了我的正常目标——以一种可管理的方式允许2个线程相互信号完成
  • 我很少需要 Mutex 等的其他功能(例如跨进程)

嘿,感谢迅速回复。 关于“over what”问题 - 在使用 Monitor.Enter 和 Monitor.Exit 方面, 我并没有真正看出 Pulse 和 Wait 与使用这两种方法有多大区别 - 或许只是在性能成本方面。 - seren1ty
@seren1ty,它们执行的功能是完全不同的;Enter/Exit获取和释放锁;Wait释放锁,进入等待队列(等待脉冲),然后(在唤醒时)重新获取锁;Pulse将等待项从等待队列移动到就绪队列。这是完全不同(但互补)的用法。Pulse/Wait用于协调线程之间的操作,而不需要热循环或冷循环。 - Marc Gravell

4
你的代码片段存在一个严重的缺陷,当SecondThreadTwo()在空队列上调用Dequeue()时,将会失败。你可能是通过让FirstThreadTwo()在消费者线程之前执行了几分之一秒来使其正常工作的。这是一个偶然事件,而且在运行这些线程一段时间后或在不同的机器负载下启动它们后,这种方法将停止工作。这可能会意外地无错误地工作相当长的一段时间,很难诊断偶尔的故障。
使用锁语句没有办法编写阻塞消费者线程直到队列变为非空的锁定算法。不断进入和退出锁的忙碌循环虽然可行,但是效果很差。
编写这种代码最好留给线程专家,因为很难证明它在所有情况下都有效。不仅要避免像这个错误模式或线程竞争这样的故障,还要确保算法的一般适应性,以避免死锁、活锁和线程车队等问题。在.NET世界中,专家是Jeffrey Richter和Joe Duffy。他们吃掉锁定设计的早餐,无论是在他们的书籍、博客还是杂志文章中。窃取他们的代码是被期望和接受的。并且部分进入了System.Collections.Concurrent命名空间中的附加内容。

3

使用Monitor.Pulse/Wait可以提高性能,正如您所猜测的那样。获取锁是一个相对昂贵的操作。通过使用Monitor.Wait,您的线程会休眠,直到另一个线程使用'Monitor.Pulse'唤醒它。

在任务管理器中,您将看到差异,因为即使队列中没有任何内容,一个处理器核心也会被占用。


0

PulseWait的优点在于它们可以用作所有其他同步机制(包括互斥锁、事件、屏障等)的构建块。使用PulseWait可以完成其他BCL中任何同步机制无法完成的任务。

所有有趣的事情都发生在Wait方法内部。Wait将退出临界区并将线程置于等待队列中,使其处于WaitSleepJoin状态。一旦调用Pulse,则等待队列中的下一个线程将移动到就绪队列中。一旦线程切换到Running状态,它将重新进入临界区。这是重要的另一种重复方式。Wait将以原子方式释放锁并重新获取锁。没有其他同步机制具有此功能。

最好的方法是尝试使用其他策略来复制行为,然后看看可能出现什么问题。让我们尝试使用ManualResetEvent进行这个练习,因为SetWaitOne方法似乎是类似的。我们的第一次尝试可能看起来像这样。
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。如果你的第二个代码块需要该条件为true,那肯定会导致很多奇怪的问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接