Monitor.Enter与Monitor.Wait的区别

7

我仍然不确定这两个调用之间的区别。根据MSDN文档,

Monitor.Enter(Object)会在指定对象上获取独占锁。

Monitor.Wait(Object)会释放一个对象上的锁并阻塞当前线程,直到重新获取锁。

由此我认为Monitor.Wait与Monitor.Enter相同,只是在重新获取锁之前先释放了对象上的锁。

当前线程是否必须首先拥有锁?不同的线程如何强制释放对象的锁?为什么同一线程需要重新获取锁?


3
进入/退出用于互斥,等待/通知(全部)用于等待和信号传递。详见http://en.wikipedia.org/wiki/Monitor_(synchronization)。 - dtb
3个回答

9
根据MSDN:Monitor.Wait方法(Object),只有当你已经拥有锁时才能调用Monitor.Wait(Object),而你需要调用Monitor.Enter(Object)来获取锁。换句话说,如果调用线程没有指定对象的锁,则会出现SynchronizationLockException异常。至于为什么需要Monitor.Wait:如果线程意识到它缺少继续执行所需的信息(例如,它正在等待信号),则可能希望让其他线程进入关键部分,因为并非所有线程都具有相同的前提条件。要使等待线程继续执行,您需要在释放锁之前调用Monitor.Pulse(Object)Monitor.PulseAll(Object)(否则,您将获得与Monitor.Wait(Object)相同类型的异常)。
请记住,在脉冲和释放锁之后,获得锁的下一个线程不一定是收到脉冲的线程。
还要牢记,接收脉冲并不等同于满足条件。你可能仍然需要等待片刻:
// make sure to synchronize this correctly ;)
while (ConditionNotMet)
{
    Monitor.Wait(mutex);
    if (ConditionNotMet) // We woke up, but our condition is still not met
        Monitor.Pulse(mutex); // Perhaps another waiting thread wants to wake up?
}

3
考虑以下示例:
public class EnterExitExample
{
    private object myLock;
    private bool running;

    private void ThreadProc1()
    {
        while (running)
        {
            lock (myLock)
            {
                // Do stuff here...
            }
            Thread.Yield();
        }
    }

    private void ThreadProc2()
    {
        while (running)
        {
            lock (myLock)
            {
                // Do other stuff here...
            }
            Thread.Yield();
        }
    }
}

现在你有两个线程,每个线程都在等待锁,然后执行它们的任务,最后释放锁。lock (myLock)语法只是Monitor.Enter(myLock)Monitor.Exit(myLock)的简化形式。
现在让我们看一个更复杂的例子,其中涉及到WaitPulse
public class PulseWaitExample
{
    private Queue<object> queue;
    private bool running;

    private void ProducerThreadProc()
    {
        while (running)
        {
            object produced = ...; // Do production stuff here.
            lock (queue)
            {
                queue.Enqueue(produced);
                Monitor.Pulse(queue);
            }
        }
    }

    private void ConsumerThreadProc()
    {
        while (running)
        {
            object toBeConsumed;
            lock (queue)
            {
                Monitor.Wait(queue);
                toBeConsumed = queue.Dequeue();
            }
            // Do consuming stuff with toBeConsumed here.
        }
    }
}

这是什么?

生产者在任何时候都可以生产对象。一旦他生产出一个对象,就会获得队列的锁,并将该对象入队,然后进行 Pulse 调用。

与此同时,消费者没有锁定,他通过调用 Wait 来释放锁。一旦他在该对象上收到 Pulse,他将重新锁定并执行消费操作。

因此,您在这里拥有直接的线程通知,告诉消费者有事情要做。如果没有这个,您只能让消费者不断轮询集合以查看是否还有任务需要处理。使用 Wait,您可以确保有任务需要处理。


从另一个回答的角度来看,“只有在拥有锁时才能调用Monitor.Wait(Object)” 。当生产者拥有锁时,消费者可以调用Monitor.Wait(queue)吗? - Adam A
生产者在短时间内拥有锁,这段时间包括了Queue.EnqueueMonitor.Pulse的调用,后者会释放锁。消费者要么完全不在锁块之内,这意味着他只需获取锁,调用Wait,由于持续的Pulse而立即停止等待;或者他正在Wait调用中,这使得他重新获取锁并执行消费操作。 - Jan Dörrenhaus
根据关于Monitor.Pulse()的文档,这看起来像是一个死锁等待的情况:“Monitor类没有维护状态表明已调用Pulse方法。因此,如果在没有线程等待时调用Pulse方法,则下一个调用Wait的线程会被阻塞,就好像从未调用过Pulse一样。如果两个线程正在使用Pulse和Wait进行交互,这可能会导致死锁。与AutoResetEvent类的行为相反[...]” - Cristian Diaconescu
1
我已经尝试了这个例子。http://ideone.com/ucE2PY - 它不会死锁(生产者一直在发出脉冲),但是仍然会丢失一些脉冲。生产者总是会生产比消费者更多的元素。每当消费者在其“锁定”块之外时,生产者发出脉冲,那个脉冲就会丢失。 - Cristian Diaconescu

1
正如Cristi所提到的,一个天真的wait/pulse代码是不起作用的。因为你完全忽略了关键点:监视器不是消息队列。如果你脉冲而没有人在等待,脉冲就会丢失。 正确的哲学是,你正在等待一个条件,如果条件不满足,有一种方法可以等待它,而不会占用CPU和锁定。在这里,消费者的条件是队列中有东西。 请参见https://ideone.com/tWqTS1,它可以工作(从Cristi的示例分叉)。
public class PulseWaitExample
{
    private Queue<object> queue;
    private bool running;

    private void ProducerThreadProc()
    {
        while (running)
        {
            object produced = ...; // Do production stuff here.
            lock (queue)
            {
                queue.Enqueue(produced);
                Monitor.Pulse(queue);
            }
        }
    }

    private void ConsumerThreadProc()
    {
        while (running)
        {
            object toBeConsumed;
            lock (queue)
            {
                // here is the fix
                if (queue.Count == 0)
                { 
                   Monitor.Wait(queue);
                }
                toBeConsumed = queue.Dequeue();
            }
            // Do consuming stuff with toBeConsumed here.
        }
    }
}

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