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