Підрозділ 15.4
Клас Monitor
15.4. Клас Monitor Оператор lock — це зручна синтаксична обгортка над класом System.Threading.Monitor . Кожен lock блок компілятор перетворює на виклики Monitor.Enter і Monitor.Exit у блоці try/finally . Тобто
15.4. Клас Monitor
Оператор lock — це зручна синтаксична обгортка над класом System.Threading.Monitor. Кожен lock-блок компілятор перетворює на виклики Monitor.Enter() і Monitor.Exit() у блоці try/finally. Тобто коли ви пишете:
lock (lockObj)
{
// код
}Компілятор генерує приблизно таке:
bool lockTaken = false;
try
{
Monitor.Enter(lockObj, ref lockTaken);
// код
}
finally
{
if (lockTaken) Monitor.Exit(lockObj);
}Monitor надає більш низькорівневий і гнучкий API, ніж lock. Його ключова перевага — можливість очікування та сигналізації: потік може добровільно звільнити замок і перейти в режим очікування, а інший потік може «розбудити» його, коли настане відповідний момент. Саме ця можливість дозволяє реалізовувати складні патерни координації між потоками — наприклад, «виробник — споживач».
Методи Monitor
Ключові методи класу Monitor:
| Метод | Опис |
|---|---|
Monitor.Enter(obj) |
Захоплює замок на об'єкті obj. Блокує потік, якщо замок зайнятий |
Monitor.Exit(obj) |
Звільняє замок. Викликати лише у потоці, що тримає замок |
Monitor.TryEnter(obj) |
Спроба захопити замок без блокування. Повертає bool |
Monitor.TryEnter(obj, ms) |
Спроба захопити замок із таймаутом ms мілісекунд |
Monitor.Wait(obj) |
Звільняє замок і блокує поточний потік до сигналу Pulse/PulseAll. Замок автоматично повертається при пробудженні |
Monitor.Pulse(obj) |
Сповіщає один потік, що очікує через Wait, про можливість продовження |
Monitor.PulseAll(obj) |
Сповіщає всі потоки, що очікують через Wait |
Методи Wait, Pulse і PulseAll можуть викликатись лише у потоці, що тримає замок — тобто всередині lock-блоку або між Enter() і Exit(). Порушення цієї умови призведе до SynchronizationLockException.
Monitor.Wait і Monitor.Pulse: механізм координації
Monitor.Wait(obj) виконує три дії атомарно:
- Звільняє замок на
obj - Переводить поточний потік у список очікування для
obj - Блокує поточний потік
Коли інший потік викликає Monitor.Pulse(obj), один потік з цього списку отримує сигнал і може відновити виконання — але спочатку він зобов'язаний знову захопити замок (що відбувається автоматично при виході з Wait).
Ця схема є основою класичного патерну «виробник — споживач»: виробник додає дані до черги і сповіщає споживача через Pulse; споживач чекає через Wait, доки черга не буде заповнена.
Клінічний приклад: черга лабораторних зразків
У клінічній лабораторії зразки надходять від медсестер (виробники) і обробляються лаборантами (споживачі). Черга зразків — це спільний ресурс, доступ до якого потрібно синхронізувати.
Зверніть на конструкцію while (_samples.Count == 0 && _accepting) { Monitor.Wait(_lock); }. Умова перевіряється у циклі, а не в if — це критично важливо. Pulse не гарантує, що після пробудження умова дійсно виконана: можливе так зване «хибне пробудження» (spurious wakeup), або інший потік міг встигнути забрати зразок до того, як поточний захопив замок. Тому завжди перевіряємо умову заново після Wait.
Кілька споживачів з PulseAll
Monitor.Pulse сповіщає лише один потік; Monitor.PulseAll — всі. Якщо є кілька лаборантів, при додаванні партії зразків доцільно розбудити всіх одразу — хай кожен забере по одному:
Monitor.TryEnter — спроба без блокування
Monitor.TryEnter(obj) намагається захопити замок і повертає true або false негайно, без очікування. Це корисно, коли потік має корисну альтернативну роботу, якщо замок зайнятий:
TryEnter є важливим інструментом для уникнення дедлоків: якщо ресурс недоступний, потік не «зависає» у вічному очікуванні, а може спробувати іншу стратегію.
Monitor vs lock: коли що обирати
lock підходить для переважної більшості задач: захист критичної секції, яка виконується коротко і не потребує координації між потоками. Monitor обирають у таких ситуаціях:
- Потрібні
Wait/Pulse/PulseAllдля координації «виробник — споживач» або подібних патернів - Потрібен
TryEnterдля спроби захоплення без блокування, зокрема з таймаутом - Потрібен більш гранульований контроль над захопленням/звільненням замку
У всіх інших сценаріях — lock зрозуміліший, компактніший і менш схильний до помилок (наприклад, забутого Monitor.Exit()).