Использование Pulse и Wait

Итак, начнем. И для начала условимся о следующем:

§ Из всех конструкций синхронизации мы будем использовать только lock aka Monitor.Enter / Monitor.Exit.

§ Нет никаких ограничений на загрузку CPU!

Имея в виду эти два правила, рассмотрим простой пример: рабочий поток, который приостанавливается, пока не получит уведомление от главного потока:

class SimpleWaitPulse { bool go; object locker = new object(); void Work() { Console.Write("Ждем... "); lock (locker) { while (!go) { // Освободим блокировку, чтобы другой поток мог изменить флаг go Monitor.Exit(locker); // Снова заблокируем перед проверкой go в while Monitor.Enter(locker); } } Console.WriteLine("Оповещен!"); } void Notify()// вызывается из другого потока { lock (locker) { Console.Write("Оповещаем... "); go = true; } } }

Вот метод Main, приводящий все это в движение:

static void Main() { SimpleWaitPulse test = new SimpleWaitPulse(); // Запускаем метод Work в отдельном потоке new Thread(test.Work).Start(); // "Ждем..." // Подождем секунду и уведомим рабочий поток из главного: Thread.Sleep(1000); test.Notify(); // "Оповещаем... Оповестили!" }

Метод Work, где мы крутимся в цикле, постоянно потребляет ресурсы CPU, пока флаг go установлен в true! В цикле нужно постоянно переключать блокировку при помощи Monitor.Enter и Monitor.Exit – чтобы другой поток мог получить блокировку и модифицировать флаг go. Доступ к полю go должен всегда осуществляться только изнутри lock, чтобы избежать проблем с асинхронной изменчивостью (volatility) (помните, что по правилам, о которых мы условились, другие конструкции синхронизации, в том числе и ключевое слово volatile нам недоступны!).

Теперь запустим пример, чтобы убедиться, что он действительно работает. Вот что он выводит:

Ждем... (пауза) Оповещаем... Оповестили!

Добавим Wait и Pulse. Сделаем это так:

§ Заменим переключение блокировки (Monitor.Enter / Monitor.Exit) на Monitor.Wait.

§ Вставим вызов Monitor.Pulse после установки флага go.

Вот модифицированный класс, с опущенными для краткости вызовами Console:

class SimpleWaitPulse { bool go; object locker = new object(); void Work() { lock (locker) while (!go) Monitor.Wait(locker); } void Notify() { lock (locker) { go = true; Monitor.Pulse(locker); } } }

Класс работает так же, как и раньше, только постоянная прокрутка цикла устранена. Wait неявно исполняет код, который был удален – Monitor.Enter после Monitor.Exit, но с одним дополнительным шагом в середине: пока блокировка отпущена, он ожидает вызова Pulse из другого потока. Именно это и делает метод Notifier после установки флага go в true. Работа сделана.


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



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