24 Возможности библиотеки Task Parallel Library. Monitor

Конструкция lock введена для удобства как аналог применения объекта синхронизации Monitor.

Итак, конструкция
lock(sync_obj) 
{ 
  // Critical section 
} 
  
аналогична применению объекта Monitor:
try 
{ 
  Monitor.Enter(sync_obj); 
  // Critical section 
} 
finally 
{ 
  Monitor.Exit(sync_obj); 
} 
  
Блоки try-finally формируются для того, чтобы гарантировать освобождение блокировки (критической секции) в случае возникновения какого-либо исключения внутри критической секции.


Кроме "обычного" входа в критическую секцию класс Monitor предоставляет "условные" входы:
b = Monitor.TryEnter(sync_obj); 
if(!b) 
{ 
  // Выполняем полезную работу 
  DoWork(); 
  // Снова пробуем войти в критическую секцию 
  Monitor.Enter(sync_obj); 
} 
// Критическая секция  
ChangeData(); 
// Выходим 
Monitor.Exit(sync_obj); 
  
Если критическая секция уже выполняется кем-то другим, то поток не блокируется, а выполняет полезную работу. После завершения всех полезных работ поток пытается войти в критическую секцию с блокировкой.

Если доступ к критической секции достаточно интенсивен со стороны множества потоков, то полезным может быть метод TryEnter с указанием интервала в миллисекундах, в течение которого поток пытается захватить блокировку.
while(! Monitor.TryEnter(sync_obj, 100)) 
{ 
  // Полезная работа 
  DoWork(); 
} 
// Критическая секция  
ChangeData(); 
// Выходим 
Monitor.Exit(sync_obj); 
  
Объект Monitor также предоставляет методы для обмена сигналами с ожидающими потоками Pulse и Wait, которые могут быть полезны для предотвращения взаимоблокировки в случае работы с несколькими разделяемыми ресурсами.

В следующем фрагменте два потока пытаются захватить ресурсы P и Q. Первый поток захватывает сначала P, затем пытается захватить Q. Второй поток сначала захватывает ресурс Q, а затем пытается захватить P. Применение обычной конструкции lock привело бы в некоторых случаях к взаимоблокировке потоков – потоки успели захватить по одному ресурсу и пытаются получить доступ к недостающему ресурсу. Следующий фрагмент решает проблему с помощью объекта Monitor:
void ThreadOne() 
{ 
  // Получаем доступ к ресурсу P 
  Monitor.Enter(P); 
  // Пытаемся захватить ресурс Q 
  if(!Monitor.TryEnter(Q)) 
  { 
    // Если Q занят другим потоком, 
    // освобождаем P и  
    // ожидаем завершения работы потока 
    Monitor.Wait(P); 
    // Освободился ресурс P, смело захватываем и Q 
    Monitor.Enter(Q); 
  } 
  // Теперь у потока есть и P, и Q, выполняем работу 
  .. 
  // Освобождаем ресурсы в обратной последовательности  
  Monitor.Exit(Q); 
  Monitor.Exit(P); 
} 
     
void ThtreadTwo() 
{ 
  Monitor.Enter(Q); 
  Monitor.Enter(P); 
  // Выполняем необходимую работу 
  .. 
  // Обязательный сигнал для потока, который  
  // заблокировался при вызове Monitor.Wait(P) 
  Monitor.Pulse(P); 
  Monitor.Exit(P); 
  Monitor.Exit(Q); 
} 
  
Первый поток после захвата ресурса Р пытается захватить Q. Если Q уже занят, то первый поток, зная, что второму нужен еще и P, освобождает его и ждет завершения работы второго потока с обеими ресурсами. Вызов Wait блокирует первый поток и позволяет другому потоку (одному из ожидающих) войти в критическую секцию для работы с ресурсом P. Работа заблокированного потока может быть продолжена после того как выполняющийся поток вызовет метод Pulse и освободит критическую секцию. Таким образом, первый поток возобновляет работу не после вызова Pulse, а после вызова Exit(P).