Чем плоха функция PulseEvent

Лично мне нравится функция PulseEvent. Она освобождает и тут же вновь захватывает объект событие. Но, как справедливо отметили в комментариях к одному из моих прошлых постов, компания Microsoft не рекомендует ее к использованию. Вместо нее, начиная с Windows Vista, рекомендуется использовать механизм условных переменных (condition variable). Функция PulseEvent сохранена исключительно в целях обратной совместимости. Но давайте попробуем разобраться, чем она так плоха и можно ли с этим что-то сделать?

Первый и главный недостаток состоит в том, что у вас нет гарантий относительно того, пробудился ли ожидающий поток. Операционная система может временно изъять поток из очереди ожидания для выполнения своих внутренних задач. Если вызов PulseEvent придется на этот отрезок времени, то поток не заметит, что вы ему сигналили. После возвращения в очередь он вновь заблокируется на объекте событие. Более подробно об этом можно прочесть в статье.

К сожалению красивого способа обойти эту проблему, по всей видимости не существует (иначе Реймонд Чен привел бы его в своей статье). Ниже я кратко опишу костыльное решение. Вспомним, исходная функция потока у нас выглядела примерно так:

while(true)
{
  WaitForSingleObject(hEvent_, INFINITE);
  
  if(NeedStop)
    break;

  WorkDone = false;
  
  //Делаем что-то полезное

  WorkDone = true; 
}

Здесь hEvent_ — объект событие, по которому пробуждается поток, WorkDone — признак того, что поток выполнил всю работу.

Для гарантированного запуска потока нам потребуется таймер. Запуск потока будет иметь вид:

timerUnblockThread->Enabled = true;

Мы просто включаем таймер и ничего больше. Код функции таймера приведен ниже

if(thread->isWorkDone())
  thread->Pulse();
else
  timerUnblockThread->Enabled = false;

Каждый раз мы проверяем: завершил ли поток работу? Если да, то это значит, что он к ней еще не приступил (логика установки признака WorkDone описана в посте). Значит он заблокирован, и мы должны его разблокировать. Для этого мы вызываем метод Pulse (обёртка над функцией PulseEvent). Если же поток не завершил работу, то мы свою работу выполнили и отключаем таймер.

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

Также возможна ситуация, при которой таймер срабатывает в тот момент, когда поток разблокировался, но еще не установил признак WorkDone. В этом случае произойдет лишний вызов PulseEvent. Поскольку PulseEvent для не ожидающего потока ничего не делает, это не страшно. Пусть вызывается. Да, это лишняя работа, но не смертельно.

Второй недостаток состоит в том, что вы не можете знать, какой поток будет пробужден. Например, если на некотором объекте событие заблокировано три потока A, B и C, и поток D вызывает PulseEvent для этого объекта событие, то вы не можете знать точно какой из потоков (A, B, C или вообще никакой) будет разблокирован.

Решение данной проблемы: использовать для каждого потока свой объект событие. В этом случае PulseEvent может разбудить только один заранее известный нам поток. Собственно так и сделано в классе TEventThread (см. его описание в посте). В нём объект событие инкапсулирован в сам класс. И пользователь класса ничего не знает о нём.

Время создания потока

А что если пойти другим путём? Что если мы будем создавать рабочий поток не в начале работы программы, а тогда, когда он действительно понадобится, тогда, когда для него появится работа? В этом случае нам не нужен «спусковой крючок» для запуска потока, а значит и не нужна функция PulseEvent.

При каждом запуске задачи (не программы) мы будем создавать рабочий поток заново, а по окончании работы он будет уничтожаться. В этом случае мы избавляемся от многих проблем синхронизации и всех проблем, связанных с функцией PulseEvent. Главное в этом случае корректно освободить все ресурсы, связанные с рабочим потоком и не допустить одновременного запуска нескольких рабочих потоков.

Резюме

В функции PulseEvent заложена прекрасная идея: освободить объект событие, позволить другому потоку начать свою работу, вновь захватить объект событие, и всё это в одной атомарной операции. Но реализация этой идеи получилась через одно место. Отсутствие гарантии пробуждения потока, заблокированного на объекте событие, перечеркивает всю прелесть этой идеи и заставляет изобретать костыли, которые создают новые проблемы. Поэтому мы не должны использовать ее в своих программах.

Но я не считаю ее полностью бесполезной. К ней нужно относиться как к примеру отличной идеи при хреновой реализации. «Хотели как лучше, а получилось как всегда».

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

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