0

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

线程 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 并且它们返回到循环的开头。

批量预览助手:

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

2 回答 2

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在自己的线程上运行每个实例。当然,您必须添加所有的花里胡哨才能优雅地终止它们,但这是另一回事。

于 2011-10-29T02:51:44.173 回答
0

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

您的代码执行此操作:

  1. 决定等待

  2. 将事件设置为阻止

  3. 等待事件

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

修复如下:

  1. 获取锁

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

  3. 将事件设置为阻止

  4. 释放锁

  5. 等待事件

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

于 2011-10-29T03:06:16.607 回答