监视器等待是否需要同步?

8
我开发了一个通用的生产者-消费者队列,通过监视器以以下方式脉冲:
入列:
    public void EnqueueTask(T task)
    {
        _workerQueue.Enqueue(task);
        Monitor.Pulse(_locker);
    }

"出队列:"
private T Dequeue()
    {
        T dequeueItem;
        if (_workerQueue.Count > 0)
        {
               _workerQueue.TryDequeue(out dequeueItem);
            if(dequeueItem!=null)
                return dequeueItem;
        }
        while (_workerQueue.Count == 0)
            {
                Monitor.Wait(_locker);
        }
         _workerQueue.TryDequeue(out dequeueItem);
        return dequeueItem;
    }

等待部分产生了以下的SynchronizationLockException异常: “从未同步代码块调用对象同步方法” 我需要同步它吗?为什么?使用ManualResetEvents或.NET 4.0的Slim版本更好吗?
3个回答

7
是的,当前线程需要“拥有”监视器才能调用 Wait Pulse ,这是已记录的事实。 (因此,您还需要为 Pulse 锁定。)我不知道为什么需要这样做的详细信息,但是在Java中也是一样的。 我通常会发现,无论如何都希望这样做,以使调用代码更清晰。
请注意, Wait 会释放监视器本身,然后等待 Pulse ,然后重新获取监视器再返回。
至于改用 ManualResetEvent AutoResetEvent - 您可以这样做,但我个人更喜欢使用 Monitor 方法,除非我需要等待处理程序的其他功能(例如原子地等待多个处理程序中的任何/所有处理程序)。

你为什么偏爱它?你会如何同步Monitor?只需要在Locker对象上加锁来使用Monitor吗?锁定是否会增加另一个上下文切换,而ResetEvents不需要呢? - user437631
@user437631:是的,普通的lock语句就可以了。这可能需要额外的上下文切换,也可能不需要 - 我认为你没有证据表明ResetEvents不需要它。事实上,由于它们是CLR内部对象而不是潜在的跨进程Win32对象,监视器比ResetEvents更轻量级。 - Jon Skeet

2

根据MSDN对Monitor.Wait()的描述:

释放对象上的锁并阻塞当前线程,直到重新获取锁。

“释放对象上的锁”部分是有问题的,因为对象没有被锁定。您正在将_locker对象视为WaitHandle处理。设计自己的可证明正确的锁定方式是一种黑魔法,最好留给我们的药剂师Jeffrey Richter和Joe Duffy。但我会尝试解决这个问题:

public class BlockingQueue<T> {
    private Queue<T> queue = new Queue<T>();

    public void Enqueue(T obj) {
        lock (queue) {
            queue.Enqueue(obj);
            Monitor.Pulse(queue);
        }
    }

    public T Dequeue() {
        T obj;
        lock (queue) {
            while (queue.Count == 0) {
                Monitor.Wait(queue);
            }
            obj = queue.Dequeue();
        }
        return obj;
    }
}

在大多数实际的生产者/消费者场景中,您都希望限制生产者以防止它填充无界队列。请查看Duffy的有界缓冲区设计,了解示例。如果您能够升级到.NET 4.0,那么肯定要利用其ConcurrentQueue类,它具有更多低开销锁定和自旋等黑魔法。


0

正确的理解Monitor.WaitMonitor.Pulse/PulseAll的方式不是提供等待的机制,而是(对于Wait)作为一种让系统知道代码处于等待循环中且在没有感兴趣的事情发生时无法退出的方式,以及(对于Pulse/PulseAll)作为一种让系统知道代码刚刚更改了某些可能导致满足另一个线程等待循环的退出条件的东西的方式。你应该能够将所有出现Wait的地方替换为Sleep(0),并仍然使代码正常工作(即使效率会大大降低,因为会重复使用CPU时间来测试未更改的条件)。

要使这种机制起作用,必须避免以下序列的可能性:

  • 等待循环中的代码在条件不满足时测试该条件。

  • 另一个线程中的代码更改条件,使其满足。

  • 那个其他线程中的代码脉冲锁(没有人正在等待)。

  • 等待循环中的代码执行Wait,因为它的条件未满足。

Wait方法要求等待线程具有锁,因为这是它唯一可以确保等待的条件在测试时和代码执行Wait时之间不会改变的方式。 Pulse方法需要锁定,因为这是它唯一可以确保如果另一个线程已经“承诺”执行Wait,则Pulse不会发生,直到另一个线程实际执行Wait。请注意,在锁内使用Wait不能保证其被正确使用,但在锁外使用Wait绝对不可能是正确的。

Wait/Pulse 设计如果双方合作得当,实际上可以运行得相当不错。在我看来,该设计的最大弱点是:(1)没有机制让线程等待任何一个对象被脉冲;(2)即使将一个对象“关闭”,以便所有未来的等待循环都应立即退出(可能通过检查退出标志),确保任何一个线程已经承诺的Wait会得到Pulse的唯一方法是获取锁,可能无限期地等待它变为可用。


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