ManualResetEvent - WaitOne() 似乎在某些时候没有释放线程
我有一个多线程表单应用程序,这就是相关部分的设计方式:
线程 2(BatchPreviewAssistant 类)正在等待主界面线程传递图像加载任务。接收到任务后,BatchPreviewAssistant 会将任务分配给 N=5 等待的 PrimaryLoader 线程并启用它们。 PrimaryLoaders 使用 2 个手动重置事件启动/停止无限循环:_startEvent 和 _endEvent。此外,还有一个包含 N 个手动重置事件 _parentSyncEvent 的数组,用于从 PrimaryLoaders 向 BatchPreviewAssistant 发出处理结束信号。
所以通常每个 PrimaryLoader 都在 _startEvent.WaitOne() 处等待。 一旦 BatchPreviewAssistant 需要激活它们并运行 RunPrimaryLoaders(),它会首先重置 _endEvent 和 _parentSyncEvents,然后设置 _startEvent。现在它阻塞在 WaitHandle.WaitAll(_parentSyncEvents _startEvent.Set() 导致所有 PrimaryLoader 继续进行。 每个 PrimaryLoader 完成后,就会在 _parentSyncEvent 中设置自己的事件,直到所有 5 个事件都设置完毕。此时所有 PrimaryLoader 都到达 _endEvent.WaitOne() 并等待。现在 _parentSyncEvents 已全部设置完毕,这使得 BatchPreviewAssistant 能够继续。 BatchPreviewAssistant 重置 _startEvent,然后设置 _endEvent,这将释放 PrimaryLoaders,然后它们返回到循环的开头。
BatchPreviewAssistant:
private void RunPrimaryLoaders()
{
BatchPreviewThreadsLogger.WriteLog(Common.LogLevel.Debug1, "RunPrimaryLoaders()");
ResetEvents(_parentSyncEvents);
_endEvent.Reset();
_startEvent.Set();
// Primary Loader loops restart
BatchPreviewThreadsLogger.WriteLog(Common.LogLevel.Debug2, "WaitHandle.WaitAll(_parentSyncEvent");
if (!WaitHandle.WaitAll(_parentSyncEvents, 20 * 1000))
{
throw new TimeoutException("WaitAll(_parentSyncEvent) in ProcessCurrentCommand");
// TODO: Terminate?
}
BatchPreviewThreadsLogger.WriteLog(Common.LogLevel.Message3, "Primary loading is complete");
_startEvent.Reset();
_endEvent.Set();
bool isEndEventSet = _endEvent.WaitOne(0);
BatchPreviewThreadsLogger.WriteLog(Common.LogLevel.Debug2, "isEndEventSet?" + isEndEventSet.ToString());
}
PrimaryLoader:
public void StartProc(object arg)
{
while (true)
{
BatchPreviewThreadsLogger.WriteLog(Common.LogLevel.Debug2, "Primary Loader: _startEvent.WaitOne()");
_startEvent.WaitOne();
try
{
BatchPreviewThreadsLogger.WriteLog(Common.LogLevel.Message4, "Primary Loader is processing entry:" + processingEntry.DisplayPosition.ToString());
}
catch (Exception ex)
{
BatchPreviewThreadsLogger.WriteLog(Common.LogLevel.Error, "Exception in PrimaryImageLoader.StartProc:" + ex.ToString());
}
_parentSyncEvent.Set();
BatchPreviewThreadsLogger.WriteLog(Common.LogLevel.Debug2, "Primary Loader: _endEvent.WaitOne()");
_endEvent.WaitOne();
}
}
此代码可以很好地生成数百个这样的循环,但我偶尔会遇到问题,特别是在压力测试期间。发生的情况是,当 BatchPreviewAssistant 设置 _endEvent.Set() 时,在 _endEvent.WaitOne() 处没有释放任何 PrimaryLoader;您可以看到我检查了 BatchPreviewAssistant 并看到该事件确实已设置,但 PrimaryLoaders 并未释放。
[10/27/2011;21:24:42.796;INFO ] [42-781:16]Primary Loader: _endEvent.WaitOne()
[10/27/2011;21:24:42.796;INFO ] [42-781:18]Primary Loader: _endEvent.WaitOne()
[10/27/2011;21:24:42.796;INFO ] [42-781:19]Primary Loader: _endEvent.WaitOne()
[10/27/2011;21:24:42.843;INFO ] [42-843:15]Primary Loader: _endEvent.WaitOne()
[10/27/2011;21:24:42.937;INFO ] [42-937:17]Primary Loader: _endEvent.WaitOne()
[10/27/2011;21:24:42.937;INFO ] [42-937:14]Primary loading is complete
[10/27/2011;21:24:42.937;INFO ] [42-937:14]isEndEventSet?True
这种设计是否存在任何可能导致该问题的明显问题? 我可以看到一些尝试解决问题的方法,但是很高兴看到这种方法有什么问题。
以防万一,我还提供了有关初始化和启动 PrimaryLoaders 的方式的信息。
private PrimaryImageLoader[] _primaryImageLoaders;
_primaryImageLoaders = new PrimaryImageLoader[N]
for (int i = 0; i < _primaryImageLoaderThreads.Length; i++)
{
_parentSyncEvents[i] = new AutoResetEvent(false);
_primaryImageLoaders[i] = new PrimaryImageLoader(i, _parentSyncEvents[i],
_startEvent, _endEvent,
_pictureBoxes, _asyncOperation,
LargeImagesBufferCount);
_primaryImageLoaderThreads[i] = new Thread(new ParameterizedThreadStart(_primaryImageLoaders[i].StartProc));
_primaryImageLoaderThreads[i].Start();
}
请注意,为了简化添加的示例,一些不相关的代码已被删除
: 我同意该示例太繁忙且难以遵循。简而言之:
Thread 2:
private void RunPrimaryLoaders()
{
_endEvent.Reset();
_startEvent.Set();
_startEvent.Reset();
_endEvent.Set();
bool isEndEventSet = _endEvent.WaitOne(0);
}
Threads 3-7:
public void StartProc(object arg)
{
while (true)
{
_startEvent.WaitOne();
_endEvent.WaitOne(); // This is where it can't release occasionally although Thread 2 checks and logs that the event is set
}
}
I have a multi-threading form application and this is how the part in question is designed:
Thread 2 (BatchPreviewAssistant class) is waiting for the primary interface thread to pass images load task. Once the task is received, BatchPreviewAssistant assigns tasks to N=5 waiting PrimaryLoader threads and enables them. PrimaryLoaders are running as infinite loops started/stopped using 2 manual reset events: _startEvent and _endEvent. Also, there is an array of N manual reset events _parentSyncEvent to signal end of processing from PrimaryLoaders to BatchPreviewAssistant.
So normally each PrimaryLoader is waiting at _startEvent.WaitOne().
Once BatchPreviewAssistant needs to activate them and runs RunPrimaryLoaders(), it resets _endEvent and _parentSyncEvents first and then sets _startEvent. Now it blocks at WaitHandle.WaitAll(_parentSyncEvents
The _startEvent.Set() causes all PrimaryLoader to proceed.
Once each PrimaryLoader is done, it sets its own event in _parentSyncEvent until all 5 are set. At this point all PrimaryLoaders reach _endEvent.WaitOne() and wait. Now _parentSyncEvents are all set which enables BatchPreviewAssistant to continue.
BatchPreviewAssistant resets _startEvent and then sets _endEvent which releases PrimaryLoaders and they come back to the beginning of the loop.
BatchPreviewAssistant:
private void RunPrimaryLoaders()
{
BatchPreviewThreadsLogger.WriteLog(Common.LogLevel.Debug1, "RunPrimaryLoaders()");
ResetEvents(_parentSyncEvents);
_endEvent.Reset();
_startEvent.Set();
// Primary Loader loops restart
BatchPreviewThreadsLogger.WriteLog(Common.LogLevel.Debug2, "WaitHandle.WaitAll(_parentSyncEvent");
if (!WaitHandle.WaitAll(_parentSyncEvents, 20 * 1000))
{
throw new TimeoutException("WaitAll(_parentSyncEvent) in ProcessCurrentCommand");
// TODO: Terminate?
}
BatchPreviewThreadsLogger.WriteLog(Common.LogLevel.Message3, "Primary loading is complete");
_startEvent.Reset();
_endEvent.Set();
bool isEndEventSet = _endEvent.WaitOne(0);
BatchPreviewThreadsLogger.WriteLog(Common.LogLevel.Debug2, "isEndEventSet?" + isEndEventSet.ToString());
}
PrimaryLoader:
public void StartProc(object arg)
{
while (true)
{
BatchPreviewThreadsLogger.WriteLog(Common.LogLevel.Debug2, "Primary Loader: _startEvent.WaitOne()");
_startEvent.WaitOne();
try
{
BatchPreviewThreadsLogger.WriteLog(Common.LogLevel.Message4, "Primary Loader is processing entry:" + processingEntry.DisplayPosition.ToString());
}
catch (Exception ex)
{
BatchPreviewThreadsLogger.WriteLog(Common.LogLevel.Error, "Exception in PrimaryImageLoader.StartProc:" + ex.ToString());
}
_parentSyncEvent.Set();
BatchPreviewThreadsLogger.WriteLog(Common.LogLevel.Debug2, "Primary Loader: _endEvent.WaitOne()");
_endEvent.WaitOne();
}
}
This code works pretty good making hundreds of such loops but I get an issue every once in a while, specifically during stress tests. What happens is that when BatchPreviewAssistant sets _endEvent.Set(), none of PrimaryLoaders are released at _endEvent.WaitOne(); You can see that I check in BatchPreviewAssistant and see that the event is really set, however PrimaryLoaders are not released.
[10/27/2011;21:24:42.796;INFO ] [42-781:16]Primary Loader: _endEvent.WaitOne()
[10/27/2011;21:24:42.796;INFO ] [42-781:18]Primary Loader: _endEvent.WaitOne()
[10/27/2011;21:24:42.796;INFO ] [42-781:19]Primary Loader: _endEvent.WaitOne()
[10/27/2011;21:24:42.843;INFO ] [42-843:15]Primary Loader: _endEvent.WaitOne()
[10/27/2011;21:24:42.937;INFO ] [42-937:17]Primary Loader: _endEvent.WaitOne()
[10/27/2011;21:24:42.937;INFO ] [42-937:14]Primary loading is complete
[10/27/2011;21:24:42.937;INFO ] [42-937:14]isEndEventSet?True
Is there any abvious problems with such design that may cause the issue?
I can see some ways to try as work around, however it would be nice to see what is wrong with this aproach.
Just in case I am also providing informatin on the way I initialize and start PrimaryLoaders.
private PrimaryImageLoader[] _primaryImageLoaders;
_primaryImageLoaders = new PrimaryImageLoader[N]
for (int i = 0; i < _primaryImageLoaderThreads.Length; i++)
{
_parentSyncEvents[i] = new AutoResetEvent(false);
_primaryImageLoaders[i] = new PrimaryImageLoader(i, _parentSyncEvents[i],
_startEvent, _endEvent,
_pictureBoxes, _asyncOperation,
LargeImagesBufferCount);
_primaryImageLoaderThreads[i] = new Thread(new ParameterizedThreadStart(_primaryImageLoaders[i].StartProc));
_primaryImageLoaderThreads[i].Start();
}
Please note that some irrelevant code has been removed to simplify the sample
ADDED:
I would agree that the sample is too busy and difficult to follow. So this is it in the nutshell:
Thread 2:
private void RunPrimaryLoaders()
{
_endEvent.Reset();
_startEvent.Set();
_startEvent.Reset();
_endEvent.Set();
bool isEndEventSet = _endEvent.WaitOne(0);
}
Threads 3-7:
public void StartProc(object arg)
{
while (true)
{
_startEvent.WaitOne();
_endEvent.WaitOne(); // This is where it can't release occasionally although Thread 2 checks and logs that the event is set
}
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
当你试图做一件简单的事情时,你似乎想出了一个非常复杂的设计。看起来简单的生产者/消费者模式会工作得更好,并且您不必处理手动重置事件的灾难。
您可能想要更多类似的内容:
因此,您必须在单独的线程上运行每个“Producer”实例,并且也必须在其自己的线程上运行每个“Consumer”实例。当然,您必须添加所有附加功能才能优雅地终止它们,但那是另一回事了。
It seems like you are coming up with a very complicated design when you might be trying to do a simple thing. It seems that a simple Producer/Consumer pattern would work much better and you would not have to deal with this calamity of manual reset events.
You probably want something more along the lines of this:
So you would have to run each
Producer
instance on a separate thread and eachConsumer
instance on its own thread too. Of course, you have to add in all the bells and whistles to terminate them gracefully, but that's another story.你有一个竞争条件。如果您的逻辑是检测到一个条件,将一个事件设置为阻止,然后等待该事件,则必须有一个干预解锁。
您的代码执行以下操作:
决定等待
将事件设置为阻止
等待事件
如果发生以下情况,则会出现问题该事件发生在步骤1和2之间。当我们将事件设置为阻塞时,该事件可能已经发生并解除了该事件的阻塞。当我们到达步骤 3 时,我们正在等待一个已经发生的事件来解锁它已经解锁的对象。坏的。
修复如下:
获取锁
我们需要等待吗?如果不是,释放锁并返回
将事件设置为阻塞
释放锁
等待事件
因为我们持有一个现在锁定,在我们决定等待和将事件设置为阻止之间,事件不能发生。当然,解锁事件的代码在执行处理事件和解锁事件的逻辑时必须持有相同的锁。
You have a race condition. If your logic is that you detect a condition, set an event to block, and then wait on the event, there must be an intervening unlock.
Your code does this:
Decide to wait
Set event to block
Wait on event
The problem occurs if the event occurs between steps 1 and 2. The event may have already occurred and unblocked the event when we set the event to block. When we get to step 3, we are waiting for an event that has already occurred to unblock an object it has already unblocked. Bad.
The fix is as follows:
Acquire lock
Do we need to wait? If no, release lock and return
Set event to block
Release lock
Wait on event
Because we hold a lock now, the event cannot occur between when we decide to wait and when we set the event to block. The code that unblocks the event must, of course, hold the same lock as it goes through the logic of processing the event and unblocking the event.