锁定manualResetEvent时出现死锁

may*_*yap 7 .net deadlock manualresetevent

我遇到了在锁定manualResetEvent实例时导致的死锁.我无法弄清楚如何解决它.我将不胜感激任何帮助.

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

private ManualResetEvent _event = new ManualResetEvent (true);

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

internal void Stop(){
  _event.Reset();
  lock(_event){
    ...
  }
}
Run Code Online (Sandbox Code Playgroud)

第一个线程启动了锁,并在_event.WaitOne()中被阻止;

socond线程执行了_event.Reset()行; 并在尝试执行锁定(_event)时被阻止.

我认为当在WaitOne上阻塞线程1时,应该释放锁.我想我错了.我不知道如何解决它.顺便说一句 - 我添加了锁,因为锁块中的代码应该在两个线程中同步.

再次感谢,并为长篇文章感到抱歉.

The*_*aot 10


你为什么陷入僵局

简短的答案首先:你错过了Set for Set.

我已经复制了你的代码(将大括号改为我喜欢的样式),我将在评论中解释这个问题:

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
    {
        //...
    }
}
Run Code Online (Sandbox Code Playgroud)

随着这一部分的明确,让我们继续前进到解决方案.


2.解决僵局

由于我们要.Reset()与之交换,.Set()我们还必须将ManualResetEventfrom 的默认状态更改truefalse.

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

private ManualResetEvent _event = new ManualResetEvent (false);

private void process()
{
  //...
    lock(_event)
    {
        _event.WaitOne(); //Thread A will be here waiting _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
    {
        //...
    }
}
Run Code Online (Sandbox Code Playgroud)

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


但是你有一个竞争条件......修复它.

这项工作没有完成,因为上面的代码患有种族疾病.要理解为什么想象多线程调用的情况会发生什么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
    }
}
Run Code Online (Sandbox Code Playgroud)

阅读代码中的注释以了解其工作原理.简单来说,它需要一个读写锁的允许来允许多个线程进入该方法process但只有一个进入Stop.尽管已经做了额外的工作来确保调用该方法process的线程将等到线程调用该方法Stop.


现在你遇到了折返问题......修理它.

上面的解决方案更好......而这并不意味着完美.它出什么问题了?好吧,如果你递归地调用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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

因为读写锁也允许该方法的线程Stop停止调用该方法的新线程,process同时允许那些已经存在的线程执行并等到它们完成.

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

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

那么,你我们需要所有这些......还是我们呢?

那么,这取决于你有什么行为,所以如果我确实解决了你不具备的问题,我会在下面提供一些替代解决方案.


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

Lock非常容易理解,但对我来说有点太多了......特别是如果没有必要确保每次对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(_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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

还不是正确的行为?好吧,让我们看看另一个.


6.另一种行为的替代解决方案......

这次我们将看到如何在调用方法process之前允许多个线程进入该方法Stop.

private ReaderWriterLockSlim _readWrite = new ReaderWriterLockSlim();
private int _stopGuard;

private void process()
{
    //...
    _readWrite.EnterReadLock();
    try
    {
        //...
    }
    finally
    {
        _readWrite.ExitReadLock();
    }
}

internal void Stop()
{
    if(Interlocked.CompareExchange(_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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

不是你想要的?

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


7.你已经知道的

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

private object _syncroot = new object();

private void process()
{
    //...
    lock(_syncroot)
    {
        //...
    }
}

internal void Stop()
{
    lock(_syncroot)
    {
        //...
    }
}
Run Code Online (Sandbox Code Playgroud)

7.结论

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


Cha*_* Im 3

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

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

我还建议不要同时使用 ManualResetEvent 对象作为锁。尽管编译器不会生成错误,但这可能会像您现在一样造成混乱。