Функция PulseEvent в роли спускового крючка

Функция PulseEvent предназначена для кратковременного перевода объекта событие в свободное состояние с его последующим возвратом в занятое состояние. Обычно в литературе по Windows API ей уделяется мало внимания. Тем не менее, её можно использовать в качестве спускового крючка при управлении потоком. О том, как это сделать, я сегодня и расскажу.

Предположим, что мы проектируем приложение, состоящее из двух потоков. Первый поток (назовем его управляющим) отвечает за взаимодействие с пользователем и управление вторым потоком (назовем его исполнительным). Второй же осуществляет всю черновую работу. Необходимо организовать управление исполнительным потоком.

Договоримся сразу, что весь приводимый код разрабатывался и отлаживался мной в среде C++ Builder 6. Но сама идея применима и к другим средам и языкам. Я уже неоднократно использовал этот подход, и он ни разу меня не подводил.

Поток будем реализовывать на основе класса TThread. Ниже приводится объявление нашего потока.

class TEventThread : protected TThread
{
private:
   //Описатель объекта событие
   HANDLE hEvent;
   //Счетчик
   unsigned long Count;
   //Признак необходимости остановки
   bool NeedStop;
   //Признак того, что работа выполнена
   bool WorkDone;

   //Полностью завершает работу потока
   void StopThread();

protected:
   //Функция потока
   void __fastcall Execute();

public:
   __fastcall TEventThread();
   __fastcall ~TEventThread();

   unsigned long GetCount() const {return this->Count;};
   bool IsWorkDone() const {return this->WorkDone;};

   //Пробуждает поток
   void Pulse() const {PulseEvent(this->hEvent);};
   //Останавливает расчет
   void StopCalc();
};

При наследовании от класса TThread используется квалификатор protected. Это делается для того, чтобы конечный пользователь нашего класса использовал только наш интерфейс. Наш класс содержит 4 поля:

hEvent –описатель объекта событие, с помощью которого осуществляется управление потоком;

Count – необязательный счетчик активности. Это поле используется только для демонстрации активности потока. У вас его может не быть;

NeedStop –признак необходимости остановки потока. Если его значение равно true, то поток должен прекратить свою работу. Если же его значение равно false (по умолчанию), то поток может продолжать работать;

WorkDone – признак того, что поток завершил работу. Если его значение равно true (по умолчанию), то поток полностью выполнил ту работу, которая была ему поручена. В противном случае его значение равно false.

Методы GetCount и IsWorkDone возвращают значения полей Count и WorkDone соответственно. Они нужны для того, чтобы управляющий поток мог отслеживать работу исполнительного.

Метод Pulse вызывает функцию PulseEvent для объекта hEvent.

Рассмотрим исходный код конструктора:

__fastcall TEventThread :: TEventThread() : TThread(true)
{
   this->Count = 0;
   this->NeedStop = false;
   this->WorkDone = true;
   this->FreeOnTerminate = true;
   this->hEvent = CreateEvent(NULL, false, false, NULL);
   //Пробуждаем поток
   this->Resume();
}

Обратите внимание: в списке инициализации в конструктор класса TThread мы передаем значение true. Это значит, что поток создается в приостановленном режиме. Он запускается не сразу, а тогда, когда мы сами его запустим.

В строках 3-5 инициализируются поля нашего класса. Я не использую списки инициализации, так как при большом количестве полей они делают код менее наглядным.

В 6 строке мы взводим флаг FreeOnTerminate. Это приводит к тому, что после завершения функции потока класс потока будет уничтожен автоматически. Тем самым мы упрощаем себе работу по ликвидации нашего класса.

В 7 строке мы создаем объект событие и переводим его в занятое состояние. Поскольку код конструктора исполняется управляющим потоком, то объект событие занимается им. Уничтожается данный объект в деструкторе класса TEventThread, который также исполняется управляющим потоком.

В 9 строке мы пробуждаем исполнительный поток.

Обратите внимание: управляющий поток занимает объект событие, но при этом ничего не знает о нём. Вся работа с этим объектом скрыта от управляющего потока и реализована в исполнительном потоке.

Рассмотрим функцию потока.

void __fastcall TEventThread :: Execute()
{
   while(true)
   {
      //Ждем когда нас разбудят
      WaitForSingleObject(this->hEvent, INFINITE);
      if(this->NeedStop) break;
      this->WorkDone = false;
      //Выполняем работу
      while(true)
      {
         ++(this->Count);
         Sleep(1000);
         if(this->NeedStop) break;
      }
      //Работа выполнена
      this->WorkDone = true;
   }
}

Внутри функции выполняется бесконечный цикл. При входе в него вызывается функция WaitForSingleObject, которая ждет освобождения объекта событие. Как мы помним, данный объект занят управляющим потоком. Поэтому исполнительный поток блокируется до тех пор, пока управляющий поток не освободит объект событие. Для этого он должен вызвать метод Pulse.

Предположим, управляющий поток вызывает этот метод. Исполнительный поток разблокируется, но объект событие по-прежнему останется занятым управляющим потоком.

Хорошо, исполнительный поток разблокирован. Первым делом он проверяет состояние флага NeedStop. Если он взведен, то цикл прерывается и функция потока завершает свою работу. Это приводит к завершению исполнительного потока и уничтожению его класса.

Если же флаг NeedStop сброшен (по умолчанию), то поток продолжает работу. Он сбрасывает флаг WorkDone, говоря тем самым о том, что порученная ему работа не выполнена, и приступает к этой работе.

Внутренний цикл в нашем примере имитирует работу потока над какой-то задачей. У вас это может быть вызов какой-нибудь функции. Во время этой работы функция проверяет состояние флага NeedStop. Если он взведен, то значит приказано завершиться и поток должен выйти из внутреннего цикла во внешний. Внутренний цикл может завершиться по одной из двух возможных причин:

    • исполнительный поток выполнил всю работу;

    • управляющий поток взвел флаг NeedStop, исполнительный поток обнаружил это и прервал цикл.

По окончании внутреннего цикла взводится флаг WorkDone и начинается новая итерация внешнего цикла. В ней поток вновь блокируется на объекте событие.

Обратите внимание: в функции потока флаг NeedStop никак не меняется, но его состояние регулярно проверяется. Вообще говоря, для корректной работы данного алгоритма функция потока, решающая ту или иную задачу должна регулярно проверять состояние этого флага. Если этого не делать, то управляющий поток не сможет корректно остановить работу исполнительного потока. На рисунке ниже представлена диаграмма состояний исполнительного потока.

Если взглянуть на объявление класса TEventThread, то легко видеть, что управляющий поток не имеет доступа к флагу NeedStop. Как же тогда он изменяет значение флага? Для остановки исполнительного потока, управляющий поток должен вызвать метод StopCalc. Вот его исходный код.

void TEventThread :: StopCalc()
{
   this->NeedStop = true;
   while(! this->IsWorkDone())
      Sleep(500);
   this->NeedStop = false;
}

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

Цикл ожидания работает до тех пор, пока исполнительный поток не «увидит» взведенный флаг NeedStop и не прервет внутренний цикл.

Обращаю ваше внимание: метод StopCalc должен вызываться управляющим потоком. Если его вызовет исполнительный поток, то он войдет в бесконечный цикл. Метод StopCalc в этом случае никогда не вернет управление.

Повторный сброс флага NeedStop нужен для того, чтобы предотвратить завершение работы потока при последующем вызове метода Pulse. Если его не сбросить, то при последующей разблокировке исполнительного потока, он «увидит» взведенный флаг NeedStop и завершит работу.

Для полной остановки исполнительного потока используется метод StopThread. Вот его код.

void TEventThread :: StopThread()
{
   //Останавливаем расчет
   this->StopCalc();
   this->NeedStop = true;
   this->Pulse();
}

Он вызывает метод StopCalc, чтобы остановить работу исполнительного потока и заблокировать его на объекте событие. Данный метод обсуждался выше.

Потом метод StopThread взводит флаг NeedStop и разблокирует поток (метод Pulse). Исполнительный поток, «увидев», что флаг NeedStop взведен, завершает свою работу.

Если в момент вызова метода StopThread исполнительный поток уже был заблокирован на объекте событие, то метод StopCalc сразу увидит взведенный флаг WorkDone и вернет управление.

Метод StopThread вызывается в деструкторе класса TEventThread. Поэтому вызывать его самим не нужно.

Так как метод StopThread вызывает метод StopCalc, то вызывать его из исполнительного потока нельзя.

По ссылке ниже вы можете скачать полные исходные тексты класса TEventThread и пример, демонстрирующий работу с данным классом.

Класс TEventThread в таком виде не очень удобен для работы. Я реализовал его именно так, чтобы продемонстрировать вам алгоритм использования функции PulseEvent в роли спускового крючка. Этот подход удобен, когда вы хотите иметь возможность перезапускать поток, не создавая его каждый раз заново.

One reply

  1. Rika:

    Статья-сборник антипаттернов.

    Функция PulseEvent сломана и её нельзя использовать вообще никогда, о чём большими буквами сказано в документации. Начиная с Windows Vista её функцию выполняет CONDITION_VARIABLE, на Windows XP их можно эмулировать через другие примитивы, например, CRITICAL_SECTION + два ивента (см. Strategies for Implementing POSIX Condition Variables on Win32).

    Не нужно поллить (опрашивать состояние) со Sleep, нужно ввести второе событие, выставлять его вместе с выставлением Done = true и ждать завершения на нём.

    Вся работа с внутренним состоянием — NeedStop, WorkDone и т. д. — должна выполняться под блокировкой. В момент, когда эта блокировка отпускается, чтобы подождать на событии, или когда она перезахватывается назад, возникает гонка, поэтому объект, проснувшийся от ожидания, должен после захвата блокировки перепроверить условие, которого он ждал.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *