【问题标题】:ManualResetEvent - WaitOne() does not seem to release thread at some pointManualResetEvent - WaitOne() 在某些时候似乎没有释放线程
【发布时间】:2011-12-17 17:00:11
【问题描述】:

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

线程 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());
    }

主加载器:

    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
  }
}

【问题讨论】:

  • 似乎你有一个竞争条件...... TT 除了笑话,你的例子太长而且太复杂了。设计的实际目的是什么?

标签: c# multithreading concurrency manualresetevent


【解决方案1】:

这种设计是否存在任何可能导致问题的明显问题?

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

你可能想要更多类似的东西:

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 实例。当然,您必须添加所有的花里胡哨才能优雅地终止它们,但那是另一回事了。

【讨论】:

    【解决方案2】:

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

    您的代码会这样做:

    1. 决定等待

    2. 将事件设置为阻止

    3. 等待事件

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

    修复方法如下:

    1. 获取锁

    2. 我们需要等待吗?如果没有,释放锁并返回

    3. 将事件设置为阻止

    4. 解除锁定

    5. 等待事件

    因为我们现在持有一个锁,所以在我们决定等待和将事件设置为阻塞之间不会发生事件。解锁事件的代码当然必须在处理事件和解锁事件的逻辑过程中持有相同的锁。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-01-05
      • 2012-07-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多