- 为什么你会陷入僵局
首先是简短的回答:您错过了设置的重置。
我已经复制了您的代码(将大括号更改为我喜欢的样式),我将在评论中解释问题:
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
{
//...
}
}
清楚了那部分,让我们继续解决问题。
- 解决僵局
由于我们要与之交换.Reset()
,.Set()
我们还必须更改ManualResetEvent
fromtrue
到的默认状态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
.
- 但是你有一个比赛条件......修复它。
这项工作没有完成,因为上面的代码患有竞争条件。要理解为什么要想象在多个线程调用的情况下会发生什么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
}
}
阅读代码中的注释以了解其工作原理。简单来说,就是利用了读写锁,允许多个线程进入方法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
}
}
}
为什么我们需要读写锁?- 你可能会问 - 如果我们使用锁来确保只有一个线程进入方法Stop
...?
因为读写锁还允许方法处的线程Stop
停止正在调用该方法的较新线程,process
同时允许那些已经存在的线程执行并等待它们完成。
为什么我们需要ManualResetEvent
?- 你可能会问 - 如果我们已经有了读写锁来控制方法中线程的执行process
......?
因为读写锁在方法被调用process
之前无法阻止方法中代码的执行。Stop
所以,你知道我们需要这一切……还是我们需要?
嗯,这取决于你有什么行为,所以如果我确实解决了一个不是你所遇到的问题,我会在下面提供一些替代解决方案。
- 具有替代行为的替代解决方案
锁很容易理解,但对我的口味来说有点过头了……特别是如果不需要确保对 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
}
}
}
还没有正确的行为?好的,让我们再看一个。
- 具有替代行为的替代解决方案......再次
这次我们将看到如何在方法被调用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(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
}
}
}
不是你想要的?
好吧,我放弃了……让我们回到基础。
- 而你已经知道的
...为了完整起见,如果您只需要确保两个方法的访问是同步的,并且您可以允许进程中的方法随时运行,那么您可以只使用锁来完成...你已经知道了。
private object _syncroot = new object();
private void process()
{
//...
lock(_syncroot)
{
//...
}
}
internal void Stop()
{
lock(_syncroot)
{
//...
}
}
- 结论
我们已经了解了为什么会发生死锁以及如何修复它,但我们也发现没有死锁并不能保证线程安全。最后,我们已经看到了具有四种不同行为和复杂性的三种解决方案(上面的第 4、5、6 和 7 点)。总而言之,我们可以得出结论,使用多线程进行开发可能是一项非常复杂的任务,我们需要保持目标清晰,并随时注意可能出现的问题。你可以说有点偏执是可以的,这不仅适用于多线程。