Обобщение модели использования Wait и Pulse

Давайте доработаем наш шаблон использования Wait и Pulse. В предыдущем примере внутри блокировки использовалось только одно булево поле – флаг go. В другом сценарии мог бы потребоваться дополнительный флаг, показывающий состояние ожидающего потока – готов или завершен. Экстраполировав эту ситуацию, и предполагая, что набор полей, вовлеченных в блокирование, может быть любым, представим программу в виде следующего псевдокода:

class X { Блокировочные поля: один или более объектов, участвующих в условии блокировки, например: bool go; bool ready; int semaphoreCount; Queue <Task> consumerQ... object locker = new object(); // защищает все перечисленные выше поля! ... SomeMethod { ... всякий раз когда нужно блокировать, основываясь на наших блокировочных полях: lock (locker) { while (! Некий набор блокировочных полей) { // Дадим шанс другим потокам изменить блокировочные поля! Monitor.Exit(locker); Monitor.Enter(locker); } } ... всякий раз когда нужно изменить одно или несколько блокировочных полей: lock (locker) { изменяем поле(поля) } } }

Теперь вставим в наш шаблон Wait и Pulse так же, как и в прошлый раз:

§ Заменим в цикле ожидания переключение блокировки на Monitor.Wait.

§ При каждом изменении условий блокировки вызываем Pulse.

Вот модифицированный псевдокод:

Wait/Pulse шаблон #1: Основной вариант использования Wait/Pulse

class X { < Блокировочные поля... > object locker = new object(); ... SomeMethod { ... ... всякий раз когда нужно блокировать, основываясь на наших блокировочных полях: lock (locker) while (!Некий набор блокировочных полей) Monitor.Wait(locker); ... всякий раз когда нужно изменить одно или несколько блокировочных полей: lock (locker) { изменяем поле(поля) Monitor.Pulse(locker); } } }

Такой подход дает надежную модель использования Wait и Pulse. Вот её главные особенности:

§ Условие блокировки реализовано с использованием некоторого набора полей (это работает и без Wait и Pulse, просто с ожиданием в цикле).

§ Wait всегда вызывается внутри цикла с проверкой условия блокирования (и внутри оператора lock).

§ Для всех Wait и Pulse и для защиты доступа ко всем блокировочным полям используется единый объект синхронизации (в примере выше – locker).

§ Блокировки кратковременны.

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

Другое полезное свойство этой модели – устойчивость логики к пропущенным Pulse. Пропуск импульсов сигнализации может произойти, когда Pulse вызывается раньше Wait, например, из-за гонок между ожидающим и сигнализирующим потоками. Поскольку в этой модели каждый сигнал означает “перепроверить условие блокировки” (а не “продолжить работу”), слишком ранний Pulse может быть безопасно проигнорирован, так как условие блокировки проверяется в while до вызова Wait.

Такой дизайн позволяет определить несколько блокировочных полей, составить из них сложное условие блокировки, но при этом использовать единственный объект синхронизации (в предыдущем примере – locker). Обычно это лучше, чем несколько объектов синхронизации, используемых в lock, Wait и Pulse, так как помогает избежать взаимоблокировок. Кроме того, с одним объектом синхронизации все блокировочные поля читаются и записываются как единое целое, тем самым исключая тонкие ошибки атомарности. Хорошей идеей, однако, будет не использовать объект синхронизации вне необходимой области видимости (объявив как private и собственно объект синхронизации, и все блокировочные поля).


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: