Опубликован: 05.03.2013 | Уровень: для всех | Доступ: платный
Лекция 4:

Средства синхронизации

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).

Mutex

Объект Mutex используется, как и Monitor, для обеспечения взаимно-исключительного доступа к фрагменту кода. В основе объекта Mutex лежит вызов функции ядра операционной системы, и поэтому блокировка с помощью Mutex является менее эффективной по сравнению с классом Monitor и конструкцией lock.

Отличие от Monitor заключается в возможности использования глобальных именованных блокировок, доступных в рамках нескольких приложений. Таким образом, с помощью объекта Mutex можно организовать синхронизацию нескольких приложений. Ядро операционной системы контролирует взаимную исключительность выполнения критических секций. Одним из примеров использования межпроцессной синхронизации является контроль количества запущенных копий приложения.

class MyApplication 
{ 
  static void Main() 
  { 
    var mutex = new Mutex(false, "MyApp ver 2.0"); 

    if(!mutex.WaitOne(TimeSpan.FromSeconds(5), false)) 
    { 
      Console.WriteLine("Приложение уже запущено"); 
      return; 
    } 
    Run(); 
    mutex.Dispose(); 
  }   

  static void Run () 
  {   
    Console.WriteLine("Welcome to MyApp ver 2.0"); 
    .. 
  } 
} 
  

В этом фрагменте создается "именованный" мьютекс. Вызов метода WaitOne позволяет одному потоку или процессу войти в критическую секцию. Вызов метода WaitOne для дополнительных копий приложения возвращает признак занятости критической секции. По этому признаку приложение распознает факт, что копия уже запущена. Временной интервал в 5с используется для случаев, если запущенное приложение завершает выполнение. Освобождение мьютекса не выполняется, так как нет необходимости передавать управление другому процессу. Второй аргумент в вызове WaitOne указывает, что используется ожидание глобального мьютекса.

Сигнальные сообщения

Сигнальные сообщения позволяют реализовать разные схемы синхронизации, как взаимное исключение, так и условную синхронизацию. При условной синхронизации поток блокируется в ожидании события, которое генерируется в другом потоке. Платформа .NET предоставляет три типа сигнальных сообщений: AutoResetEvent, ManualResetEvent и ManualResetEventSlim, а также шаблоны синхронизации, построенные на сигнальных сообщениях (CountdownEvent, Barrier). Первые два типа построены на объекте ядра операционной системы. Третий тип ManualResetEventSlim является облегченной версией объекта ManualResetEvent, является более производительным.

В следующем фрагменте два потока используют один и тот же объект типа ManualResetEvent. Первый поток выводит сообщение от второго потока. Сообщение записывается в разделяемую переменную. Вызов метода WaitOne блокирует первый поток в ожидании сигнала от второго потока. Сигнал генерируется при вызове метода Set.

void OneThread(object o) 
{ 
  ManualResetEvent mre = (ManualResetEvent)o; 
  mre.WaitOne(); 
  Console.WriteLine("Data from thread #2: " + data); 
} 
void SecondThread(object o) 
{ 
  ManualResetEvent mre = (ManualResetEvent)o; 
  Console.WriteLine("Writing data"); 
  data = "BBBBBB"; 
  mre.Set(); 
} 
  

Вывод:

       Writing data.. 
       Data from thread#2: BBBB 
  

Отличия инструментов AutoResetEvent и ManualResetEvent заключаются в режиме сброса статуса сигнального события: автоматическое (auto reset) или ручное (manual reset). Сигнал с автоматическим сбросом снимается сразу же после освобождения потока, блокированного вызовом WaitOne. Сигнал с ручным сбросом не снимается до тех пор, пока какой-либо поток не вызовет метод Reset.

В следующем фрагменте рассматриваются отличия сигнальных сообщений. Управляющий поток Manager запускает пять рабочих потоков и каждому передает один и тот же сигнальный объект. Рабочие потоки ожидают сигнала от управляющего потока.

void Worker(object initWorker) 
{ 
  string name = ((object[])initWorker)[0] as string; 
  ManualResetEvent mre =  
    (object[])initWorker)[1] as ManualResetEvent; 
  // Waiting to start work 
  mre.WaitOne(); 
  Console.WriteLine("Worker {0} starts ..", name); 
  // useful work 
} 
void Manager() 
{ 
  int nWorkers = 5; 
  Thread[] worker = new Thread[nWorkers]; 
  ManualResetEvent mre = new ManualResetEvent(false); 
  for(int i=0; i<nWorkers; i++) 
  { 
    worker[i] = new Thread(Worker); 
    worker[i].Start(new object[]{"#" + i, mre}); 
  } 
  // preparing data in shared variables for workers 
  // let start work 
  mre.Set(); 
} 
  

При установлении события mre работу начнут все ожидающие рабочие потоки. При замене объекта на AutoResetEvent событие будет сбрасываться автоматически и "поймает" его только какой-то один поток. Таким образом, объект AutoResetEvent можно использовать для реализации взаимно исключительного доступа.

static void ThreadFunc(object o) 
{ 
  var lockEvent = o as AutoResetEvent; 
  ParallelWork(); 
   
  lockEvent.WaitOne(); 
  CriticalWork(); 
  lockEvent.Set(); 

} 
static void Main() 
{ 
  Thread[] workers = new Thread[5]; 
  for(int i=0; i<5; i++) 
    workers[i] = new Thread(ThreadFunc); 
  var lockEvent = new AutoResetEvent(true); 
  for(int i=0; i<5; i++) 
    workers[i].Start(lockEvent); 

} 
  

В этом примере пять рабочих потоков часть работы могут выполнять параллельно, но какой-то фрагмент должны выполнять последовательно (критическая секция). Сообщение типа AutoResetEvent используется для организации взаимно-исключительного доступа к критической секции. Объект инициализируется с установленным сигналом для того, чтобы вначале работы один из потоков вошел в критическую секцию. При завершении выполнения критической секции поток дает сигнал одному из ожидающих потоков. Порядок вхождения потоков в критическую секцию, также как и при использовании объекта Monitor, не определен.

Объект ManualResetEventSlim функционально соответствует сигнальному событию с ручным сбросом. Применение гибридной блокировки повышает производительность в сценариях с малым временем ожидания. Вызов метода Wait в течение ограниченного промежутка времени сохраняет поток в активном состоянии (циклическая проверка статуса сигнала), если сигнал не поступил, то осуществляется вызов дескриптора ожидания ядра операционной системы.

Объекты AutoResetEvent, ManualResetEvent, а также объекты Semaphore, Mutex происходят от объекта, инкапсулирующего дескриптор ожидания ядра WaitHandle. Тип WaitHandle содержит полезные статические методы ожидания нескольких объектов синхронизации ядра:

var ev1 = new ManualResetEvent(false); 
var ev2 = new ManualResetEvent(false); 
new Thread(SomeFunc).Start(ev1); 
new Thread(SomeFunc).Start(ev2); 
// Ожидаем все сигналы  
WaitHandle.WaitAll(new ManualResetEvent[] {ev1, ev2}); 
ev1.Reset(); ev2.Reset(); 
// Ожидаем хотя бы один сигнал 
int iFirst = WaitHandle.WaitAny(new ManualResetEvent[] 
{ev1, ev2}); 
  
Светлана Папшева
Светлана Папшева
Россия
Валерий Субботин
Валерий Субботин
Украина