【问题标题】:Deadlock when locking manualResetEvent锁定manualResetEvent时出现死锁
【发布时间】:2011-03-16 08:35:53
【问题描述】:

我在锁定 manualResetEvent 实例时遇到了死锁。我不知道如何解决它。我将不胜感激。

我在一个类中有 2 个方法由不同的线程执行:

private ManualResetEvent _event = new ManualResetEvent (true);

private void process(){
  ...
  lock(_event){
    _event.WaitOne();
    ...
  }
}

internal void Stop(){
  _event.Reset();
  lock(_event){
    ...
  }
}

第一个线程获得锁并在_event.WaitOne()中被阻塞;

第二个线程执行_event.Reset();并在尝试执行 lock(_event) 时被阻止。

我认为当线程 1 在 WaitOne 上被阻塞时,锁应该被释放。我想我错了。我不知道我该如何解决。 b.t.w - 我添加了锁,因为锁块中的代码应该在两个线程中同步。

再次感谢,很抱歉发了这么长的帖子。

【问题讨论】:

    标签: .net deadlock manualresetevent


    【解决方案1】:

    1. 为什么会出现死锁

    首先简短回答:您错过了 Set 的重置。

    我已经复制了您的代码(将大括号更改为我喜欢的样式),我将在 cmets 中解释问题:

    private ManualResetEvent _event = new ManualResetEvent (true);
    
    private void process()
    {
      //...
        lock(_event)
        {
            _event.WaitOne(); //Thread A is here waiting _event to be set
            //...
        }
    }
    
    internal void Stop()
    {
        _event.Reset(); //But thread B just did reset _event
        lock(_event) //And know thread B is here waiting... nobody is going to set _event
        {
            //...
        }
    }
    

    清楚了那部分,让我们继续解决问题。


    1. 解决僵局

    由于我们要将.Reset().Set() 交换,我们还必须将ManualResetEvent 的默认状态从true 更改为false

    所以,要解决死锁,编辑代码如下[已测试]:

    private ManualResetEvent _event = new ManualResetEvent (false);
    
    private void process()
    {
      //...
        lock(_event)
        {
            _event.WaitOne(); //Thread A will be here waiting for _event to be set
            //...
        }
    }
    
    internal void Stop()
    {
        _event.Set(); //And thread B will set it, so thread a can continue
        lock(_event) //And when thread a releases the lock on _event thread b can enter
        {
            //...
        }
    }
    

    上面的代码不仅强制只有一个线程可以同时进入锁,而且进入process的线程会一直等待,直到有线程调用Stop


    1. 但是您遇到了竞争条件...正在修复它。

    工作没有完成,因为上面的代码患有竞争条件。要理解为什么要想象在多个线程调用process 的情况下会发生什么。只有一个线程会进入锁,等待Stop被调用并设置_event,之后,它可以继续。现在,考虑一下如果调用 Stops 的线程在调用 _event.Set() 之后被抢占,会发生什么情况,位于 _event.WaitOne() 的等待线程继续并离开锁......现在你无法判断是否有另一个线程正在等待进入锁定process 将进入或者如果在Stop 中被抢占的线程将继续并进入锁定该方法。这是一个竞争条件,我认为你不想要那个特定的条件。

    也就是说,我为您提供了一个更好的解决方案 [已测试]:

    private ManualResetEvent _event = new ManualResetEvent (false);
    private ReaderWriterLockSlim _readWrite = new ReaderWriterLockSlim();
    
    private void process()
    {
        //...
        _readWrite.EnterReadLock();
        _event.WaitOne();
        try
        {
            //...
        }
        finally
        {
            _readWrite.ExitReadLock();
        }
    }
    
    internal void Stop()
    {
        //there are three relevant thread positions at the process method:
        //a) before _readWrite.EnterReadLock();
        //b) before _event.WaitOne();
        //c) after _readWrite.EnterReadLock();
    
        _event.Set(); //Threads at position b start to advance
        Thread.Sleep(1); //We want this thread to preempt now!
        _event.Reset(); //And here we stop them
        //Threads at positions a and b wait where they are
        //We wait for any threads at position c
        _readWrite.EnterWriteLock();
        try
        {
            //...
        }
        finally
        {
            _readWrite.ExitWriteLock();
            //Now the threads in position a continues...
            // but are halted at position b
            //Any thread in position b will wait until Stop is called again
        }
    }
    

    阅读代码中的 cmets 以了解其工作原理。简单来说,它利用了读写锁,允许多个线程进入方法process,但只有一个线程进入Stop。尽管进行了额外的工作以确保调用方法 process 的线程将等待直到线程调用方法 Stop


    1. 现在您遇到了折返问题...正在修复它。

    上面的解决方案更好......但这并不意味着完美。它出什么问题了?好吧,如果您递归调用 Stop 或者同时从两个不同的线程调用它,它将无法正常工作,因为第二次调用可能会在第一次调用执行时使线程处于进程提前......我认为你没有不想那样。它确实看起来读写锁足以防止多个线程调用方法Stop 引起的任何问题,但事实并非如此。

    为了解决这个问题,我们需要确保 Stop 一次只执行一次。你可以用锁来做到这一点:

    private ManualResetEvent _event = new ManualResetEvent (false);
    private ReaderWriterLockSlim _readWrite = new ReaderWriterLockSlim();
    //I'm going to use _syncroot, you can use any object...
    // as long as you don't lock on it somewhere else
    private object _syncroot = new object();
    
    private void process()
    {
        //...
        _readWrite.EnterReadLock();
        _event.WaitOne();
        try
        {
            //...
        }
        finally
        {
            _readWrite.ExitReadLock();
        }
    }
    
    internal void Stop()
    {
        lock(_syncroot)
        {
            //there are three relevant thread positions at the process method:
            //a) before _readWrite.EnterReadLock();
            //b) before _event.WaitOne();
            //c) after _readWrite.EnterReadLock();
        
            _event.Set(); //Threads at position b start to advance
            Thread.Sleep(1); //We want this thread to preempt now!
            _event.Reset(); //And here we stop them
            //Threads at positions a and b wait where they are
            //We wait for any threads at position c
            _readWrite.EnterWriteLock();
            try
            {
                //...
            }
            finally
            {
                _readWrite.ExitWriteLock();
                //Now the threads in position a continues...
                // but are halted at position b
                //Any thread in position b will wait until Stop is called again
            }
        }
    }
    

    为什么我们需要读写锁? - 你可能会问 - 如果我们使用锁来确保只有一个线程进入方法Stop...?

    因为读写锁还允许方法 Stop 处的线程停止正在调用方法 process 的较新线程,同时允许已经存在的线程执行并等待它们完成。

    为什么我们需要ManualResetEvent? - 你可能会问 - 如果我们已经有了读写锁来控制方法process...中的线程的执行?

    因为在调用Stop 方法之前,读写锁无法阻止方法process 中的代码执行。

    那么,你知道我们需要这一切……还是我们需要这些?

    嗯,这取决于你有什么行为,所以如果我确实解决了一个不是你所遇到的问题,我会在下面提供一些替代解决方案。


    1. 具有替代行为的替代解决方案

    锁很容易理解,但对我的口味来说有点过头了......特别是如果不需要确保对 Stop 的每个并发调用都有机会允许线程在方法process

    如果是这样的话,你可以重写代码如下:

    private ManualResetEvent _event = new ManualResetEvent (false);
    private ReaderWriterLockSlim _readWrite = new ReaderWriterLockSlim();
    private int _stopGuard;
    
    private void process()
    {
        //...
        _readWrite.EnterReadLock();
        _event.WaitOne();
        try
        {
            //...
        }
        finally
        {
            _readWrite.ExitReadLock();
        }
    }
    
    internal void Stop()
    {
        if(Interlocked.CompareExchange(ref _stopGuard, 1, 0) == 0)
        {
            //there are three relevant thread positions at the process method:
            //a) before _readWrite.EnterReadLock();
            //b) before _event.WaitOne();
            //c) after _readWrite.EnterReadLock();
        
            _event.Set(); //Threads at position b start to advance
            Thread.Sleep(1); //We want this thread to preempt now!
            _event.Reset(); //And here we stop them
            //Threads at positions a and b wait where they are
            //We wait for any threads at position c
            _readWrite.EnterWriteLock();
            try
            {
                //...
            }
            finally
            {
                _readWrite.ExitWriteLock();
                //Now the threads in position a continues...
                // but are halted at position b
                //Any thread in position b will wait until Stop is called again
            }
        }
    }
    

    还没有正确的行为?好的,我们再看一个。


    1. 具有替代行为的替代解决方案...再次

    这次我们要看看如何在Stop方法被调用之前,让多个线程进入方法process

    private ReaderWriterLockSlim _readWrite = new ReaderWriterLockSlim();
    private int _stopGuard;
    
    private void process()
    {
        //...
        _readWrite.EnterReadLock();
        try
        {
            //...
        }
        finally
        {
            _readWrite.ExitReadLock();
        }
    }
    
    internal void Stop()
    {
        if(Interlocked.CompareExchange(ref _stopGuard, 1, 0) == 0)
        {
            //there are two relevant thread positions at the process method:
            //a) before _readWrite.EnterReadLock();
            //b) after _readWrite.EnterReadLock();
    
            //We wait for any threads at position b
            _readWrite.EnterWriteLock();
            try
            {
                //...
            }
            finally
            {
                _readWrite.ExitWriteLock();
                //Now the threads in position a continues...
                // and they will continue until halted when Stop is called again
            }
        }
    }
    

    不是你想要的?

    好吧,我放弃了……让我们回到基础。


    1. 还有你已经知道的

    ...为了完整起见,如果你只需要确保两个方法的访问是同步的,并且你可以让进程中的方法随时运行,那么你可以只用锁...你已经知道了。

    private object _syncroot = new object();
    
    private void process()
    {
        //...
        lock(_syncroot)
        {
            //...
        }
    }
    
    internal void Stop()
    {
        lock(_syncroot)
        {
            //...
        }
    }
    

    1. 结论

    我们已经看到了为什么会发生死锁以及如何修复它,但我们也发现没有死锁并不能保证线程安全。最后,我们已经看到了具有四种不同行为和复杂性的三种解决方案(上面的第 4、5、6 和 7 点)。总而言之,我们可以得出结论,使用多线程进行开发可能是一项非常复杂的任务,我们需要保持目标清晰,并随时注意可能出现的问题。你可以说有点偏执是可以的,这不仅适用于多线程。

    【讨论】:

    • 哇非常好的例子,谢谢!现在我真的开始明白什么是线程安全了!
    • @Theraot 它给出了Recursive read lock acquisitions not allowed in this mode. 错误:( 看来我们应该把EnterReadLock(); 放在try 块内。
    【解决方案2】:

    我猜你对 Monitor.Wait(object) 和 ManualResetEvent.WaitOne() 感到困惑。

    Monitor.Wait(object) 释放锁并等待它获得锁。 ManualResetEvent.WaitOne() 阻塞当前线程,直到事件句柄得到信号。

    我也建议不要同时使用 ManualResetEvent 对象作为锁。即使编译器不会生成错误,这也可能会造成您现在可能遇到的混乱。

    【讨论】:

      最近更新 更多