OOP Course
Сьогодні

Підрозділ 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) виконує три дії атомарно:

  1. Звільняє замок на obj
  2. Переводить поточний потік у список очікування для obj
  3. Блокує поточний потік

Коли інший потік викликає 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()).

Розроблено Tomka Yurii · © 2026 ·