ManualResetEvent - WaitOne()在某些时候似乎没有释放线程
本文关键字:候似乎 线程 释放 WaitOne ManualResetEvent | 更新日期: 2023-09-27 18:12:38
我有一个多线程表单应用程序,这就是所讨论的部分是如何设计的:
线程2 (BatchPreviewAssistant类)正在等待主接口线程传递图像加载任务。接收到任务后,BatchPreviewAssistant将任务分配给N=5个等待的PrimaryLoader线程并启用它们。PrimaryLoaders作为无限循环运行,使用2个手动重置事件启动/停止:_startEvent和_endEvent。此外,还有一个N个手动重置事件数组_parentSyncEvent来表示从PrimaryLoaders到BatchPreviewAssistant的处理结束。所以通常每个PrimaryLoader都在_starteevent . waitone()等待。一旦BatchPreviewAssistant需要激活它们并运行RunPrimaryLoaders(),它首先重置_endEvent和_parentSyncEvents,然后设置_startEvent。现在它在WaitHandle处阻塞。WaitAll (_parentSyncEvents_starteevent . set()导致所有PrimaryLoader继续。一旦每个PrimaryLoader完成,它将在_parentSyncEvent中设置自己的事件,直到所有5个事件都设置完成。此时,所有的PrimaryLoaders都到达_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());
}
知荷载:
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()时,没有一个PrimaryLoaders在_endEvent.WaitOne()释放;你可以看到我在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
}
}
这样的设计是否有明显的问题可能导致问题?
当你试图做一件简单的事情时,你似乎想出了一个非常复杂的设计。似乎一个简单的生产者/消费者模式会工作得更好,你不必处理这种手动重置事件的灾难。
您可能想要更多类似这样的内容:
class Producer
{
private readonly BlockingQueue<Task> _queue;
public Producer(BlockingQueue<Task> queue)
{
_queue = queue;
}
public LoadImages(List<Task> imageLoadTasks)
{
foreach(Task t in imageLoadTasks)
{
_queue.Enqueue(task);
}
}
}
class Consumer
{
private volatile bool _running;
private readonly BlockingQueue<Task> _queue;
public Consumer(BlockingQueue<Task> queue)
{
_queue = queue;
_running = false;
}
public Consume()
{
_running = true;
while(_running)
{
try
{
// Blocks on dequeue until there is a task in queue
Task t = _queue.Dequeue();
// Execute the task after it has been dequeued
t.Execute();
}
catch(ThreadInterruptedException)
{
// The exception will take you out of a blocking
// state so you can check the running flag and decide
// if you need to exit the loop or if you shouldn't.
}
}
}
}
因此,您必须在单独的线程上运行每个Producer
实例,并且每个Consumer
实例也在自己的线程上运行。当然,您必须添加所有的附加功能来优雅地终止它们,但那是另一回事。
您有一个竞争条件。如果你的逻辑是你检测到一个条件,设置一个事件阻塞,然后等待事件,必须有一个中间的解锁。
你的代码是这样做的:
-
决定等待
-
设置事件阻塞
-
等待事件
如果事件发生在步骤1和步骤2之间,则出现问题。当我们将事件设置为阻塞时,事件可能已经发生并解除了阻塞。当我们到达步骤3时,我们正在等待一个已经发生的事件来解除它已经解除阻塞的对象的阻塞。坏。
修复如下:
-
获取锁
-
我们需要等待吗?如果没有,则释放锁并返回
-
设置事件阻塞
-
释放锁
-
等待事件
因为我们现在持有锁,所以在我们决定等待和将事件设置为阻塞之间,事件不会发生。当然,解除事件阻塞的代码在处理事件和解除事件阻塞的逻辑过程中必须持有相同的锁。