ManualResetEvent - WaitOne()似乎在某些时候没有释放线程

时间:2011-10-29 02:32:19

标签: c# multithreading concurrency manualresetevent

我有一个多线程表单应用程序,这就是设计相关部分的方式:

线程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个都设置完毕。此时所有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());
    }

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()中不会释放任何PrimaryLoaders;你可以看到我检查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
  }
}

2 个答案:

答案 0 :(得分:2)

  

此类设计是否存在可能导致问题的明显问题?

当你尝试做一件简单的事情时,你似乎想出了一个非常复杂的设计。似乎一个简单的生产者/消费者模式可以更好地工作,你不必处理这种手动重置事件的灾难。

您可能想要更多内容:

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 :(得分:0)

你有竞争条件。如果您的逻辑是您检测到条件,将事件设置为阻止,然后等待事件,则必须有一个介入解锁。

您的代码执行此操作:

  1. 决定等待

  2. 将事件设置为阻止

  3. 等待活动

  4. 如果事件发生在步骤1和2之间,则会出现问题。当我们将事件设置为阻止时,事件可能已经发生并取消阻止事件。当我们进入第3步时,我们正在等待已经发生的事件来解锁已经解除阻塞的对象。坏。

    修复方法如下:

    1. 获取锁定

    2. 我们需要等待吗?如果不是,请释放锁定并返回

    3. 将事件设置为阻止

    4. 释放锁定

    5. 等待活动

    6. 因为我们现在持有锁,所以在我们决定等待和将事件设置为阻止之间不会发生事件。当然,取消阻止事件的代码必须保持与处理事件和解除阻塞事件的逻辑相同的锁定。