ManualResetEvent - WaitOne(), похоже, в какой-то момент не освобождает поток

У меня есть многопоточное приложение формы, и вот как спроектирована рассматриваемая часть:

Поток 2 (класс BatchPreviewAssistant) ожидает, пока основной поток интерфейса передаст задачу загрузки изображений. После получения задачи BatchPreviewAssistant назначает задачи N=5 ожидающим потокам PrimaryLoader и включает их. PrimaryLoaders работают как бесконечные циклы, запускаемые/останавливаемые с использованием двух событий ручного сброса: _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, и они возвращаются к началу цикла.

Помощник по пакетному просмотру:

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

person Serge    schedule 29.10.2011    source источник
comment
Похоже, у вас состояние гонки... TT Шутки в сторону, ваш пример слишком длинный и сложный, чтобы следовать ему. Какова реальная цель дизайна?   -  person Kiril    schedule 29.10.2011


Ответы (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 в своем собственном потоке. Конечно, вы должны добавить все навороты, чтобы завершить их изящно, но это уже другая история.

person Kiril    schedule 29.10.2011

У вас есть состояние гонки. Если ваша логика заключается в том, что вы обнаруживаете условие, устанавливаете событие для блокировки, а затем ожидаете события, должна быть промежуточная разблокировка.

Ваш код делает это:

  1. Решите подождать

  2. Установить событие для блокировки

  3. Подождите события

Проблема возникает, если событие происходит между шагами 1 и 2. Возможно, событие уже произошло и разблокировало событие, когда мы установили для него блокировку. Когда мы доходим до шага 3, мы ждем уже произошедшего события, чтобы разблокировать уже разблокированный объект. Плохой.

Исправление заключается в следующем:

  1. Получить блокировку

  2. Нам нужно ждать? Если нет, снять блокировку и вернуться

  3. Установить событие для блокировки

  4. Снять блокировку

  5. Подождите события

Поскольку сейчас мы удерживаем блокировку, событие не может произойти между моментом, когда мы решили подождать, и моментом, когда мы установили для события блокировку. Код, который разблокирует событие, должен, конечно, удерживать ту же блокировку, поскольку он проходит через логику обработки события и разблокировки события.

person David Schwartz    schedule 29.10.2011