Итак, начнем. И для начала условимся о следующем:
§ Из всех конструкций синхронизации мы будем использовать только 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. Работа сделана.