首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >锁定manualResetEvent时出现死锁

锁定manualResetEvent时出现死锁
EN

Stack Overflow用户
提问于 2011-03-16 16:35:53
回答 2查看 2.7K关注 0票数 11

我遇到一个在锁定manualResetEvent实例时导致的死锁。我想不出怎么解决这个问题。如果有任何帮助,我将不胜感激。

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

代码语言:javascript
复制
private ManualResetEvent _event = new ManualResetEvent (true);

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

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

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

socond线程执行行_event.Reset();,但在尝试执行lock(_event)时被阻塞。

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

再次感谢并为这么长的帖子道歉。

EN

回答 2

Stack Overflow用户

发布于 2012-06-26 08:29:21

  1. 为什么会出现死锁

首先给出一个简短的答案:你错过了Set的重置。

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

代码语言:javascript
复制
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

因此,要解决死锁问题,请按照以下测试结果编辑代码

代码语言:javascript
复制
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,之后它可以继续。现在,考虑如果调用停止的线程在调用_event.Set()之后立即被抢占,则处于_event.WaitOne()的等待线程继续并离开锁...现在,您无法判断等待进入锁定process的另一个线程是否会进入,或者在Stop中被抢占的线程是否会继续进入该方法的锁定。这是一个竞态条件,我不认为你想要那个特定的条件。

也就是说,我提供了一个经过测试的更好的解决方案

代码语言:javascript
复制
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
    }
}

阅读代码中的注释以了解它是如何工作的。简单地说,它利用读写锁来允许多个线程进入方法process,但只允许一个线程进入Stop。尽管已经做了额外的工作来确保调用方法process的线程将一直等到线程调用方法Stop

  1. ,现在你有了一个再入问题...修复它。

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

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

代码语言:javascript
复制
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. Alternative solution with a alternative behaviour

锁很容易理解,但对我来说有点太难了……特别是,如果不需要确保停止的每个并发调用都有机会允许在方法process处执行线程。

如果是这种情况,那么您可以重写代码,如下所示:

代码语言:javascript
复制
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方法。

代码语言:javascript
复制
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. 和你已经知道的

...for出于完整性的考虑,如果你只需要确保两个方法的访问是同步的,并且你可以允许进程中的方法在任何时候运行,那么你可以只使用锁...你已经知道了。

代码语言:javascript
复制
private object _syncroot = new object();

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

internal void Stop()
{
    lock(_syncroot)
    {
        //...
    }
}

  1. Conclusion

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

票数 13
EN

Stack Overflow用户

发布于 2011-03-16 16:58:56

我猜你把Monitor.Wait(object)和ManualResetEvent.WaitOne()搞混了。

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

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

票数 3
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/5322739

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档