Переключение блокировки
Чтобы выполнить эту работу, Monitor.Wait временно освобождает или отключает базовый lock на время ожидания, чтобы другой поток (который будет вызывать Pulse) тоже мог получить блокировку. Метод Wait можно представить в виде следующего псевдокода:
Monitor.Exit(x); // освободить блокировку Ожидать вызова pulse для x Monitor.Enter(x); // восстановить блокировку |
Следовательно, Wait может заблокировать поток дважды: один раз при ожидании Pulse, и еще раз – при восстановлении эксклюзивной блокировки. Это также означает, что Pulse не полностью разблокирует ожидающий поток: только когда сигнализирующий поток покидает конструкцию lock, ожидающий поток действительно может идти дальше.
Переключение блокировки методом Wait эффективно независимо от уровня вложенности блокировок. Например, если Wait вызывается из двух вложенных выражений блокировки:
lock (x) lock (x) Monitor.Wait(x); |
Wait логически разворачивается следующим образом:
Monitor.Exit(x); Monitor.Exit(x); // Exit дважды для освобождения lock Ожидать вызова pulse для x Monitor.Enter(x); Monitor.Enter(x); // Восстановление уровней вложенности |
Согласно нормальной семантике блокировки, только первый вызов Monitor.Enter действительно может произвести блокировку.
Зачем нужен lock?
Почему WaitиPulse разработаны так, что могут работать только в пределах lock? Основная причина – дать возможность вызвать Wait по условию, без нарушений потоковой безопасности. В качестве простого примера предположим, что нужно вызвать Wait, только если булево поле равно false. Следующий код будет потокобезопасным:
lock (x) { if (!available) Monitor.Wait(x); available = false; } |
Несколько потоков могут выполнять этот код одновременно, но ни один не может быть вытеснен между проверкой поля и вызовом Monitor.Wait. Эти две инструкции являются атомарными. Аналогично, генерация уведомления также будет потокобезопасной:
lock (x) { if (!available) { available = true; Monitor.Pulse(x); } ... |