2010-09-26 6 views
6

Я разработал общую очередь производитель-потребитель, который пульсирует на монитор следующим образом:does Monitor.Wait Требуется синхронизация?

Епдиеие:

public void EnqueueTask(T task) 
    { 
     _workerQueue.Enqueue(task); 
     Monitor.Pulse(_locker); 
    } 

Dequeue:

private T Dequeue() 
    { 
     T dequeueItem; 
     if (_workerQueue.Count > 0) 
     { 
       _workerQueue.TryDequeue(out dequeueItem); 
      if(dequeueItem!=null) 
       return dequeueItem; 
     } 
     while (_workerQueue.Count == 0) 
      { 
       Monitor.Wait(_locker); 
     } 
     _workerQueue.TryDequeue(out dequeueItem); 
     return dequeueItem; 
    } 

секция ожидания производит следующие SynchronizationLockException: «Объектный метод синхронизации был вызван из несинхронизированного блока кода» Мне нужно его синхронизировать? Зачем ? Лучше ли использовать ManualResetEvents или Slim версию .NET 4.0?

ответ

6

Да, текущий поток должен «владеть» монитором, чтобы позвонить либо Wait, либо Pulse, как задокументировано. (Таким образом, вам нужно будет заблокировать и Pulse.) Я не знаю подробностей о том, почему это требуется, но на Java это одно и то же. Я обычно обнаружил, что все равно хочу сделать это, чтобы сделать код вызова чистым.

Отметьте, что Wait выпускает сам монитор, затем ждет Pulse, а затем снова берет монитор перед возвратом.

Что касается использования ManualResetEvent или AutoResetEvent вместо этого - вы могли бы, но лично я предпочитаю использовать Monitor методы, если я не нужны некоторые другие функции ожидания ручками (например, атомарной ждет любой/все из множества дескрипторов).

+0

Почему вы предпочитаете это? как бы вы синхронизировали монитор? просто блокировка на объекте шкафчика, используемого для монитора? doen't блокировки добавляет другое переключение контекста, которое ResetEvents не нужно? – user437631

+0

@ user437631: Да, нормальный оператор 'lock' в порядке. Это может потребовать или не потребовать дополнительного контекстного переключателя - и я не думаю, что у вас есть доказательства того, что ResetEvents не потребует этого. Фактически, поскольку они являются внутренними объектами CLR, а не потенциально перекрестно обрабатывают объекты Win32, мониторы легче ожидают, чем ResetEvents. –

2

Из описания MSDN из Monitor.Wait():

снимает блокировку на объект и блокирует текущий поток до тех пор, пока не выкупает замок.

Проблема «освобождает блокировку» - это проблема, объект не заблокирован. Вы обрабатываете объект _locker, как будто это WaitHandle. Выполнение собственного дизайна блокировки, который является оправданным, является формой черной магии, которую лучше всего оставить нашему медицину Джеффри Рихтеру и Джо Даффи. Но я дать этому выстрел:

public class BlockingQueue<T> { 
    private Queue<T> queue = new Queue<T>(); 

    public void Enqueue(T obj) { 
     lock (queue) { 
      queue.Enqueue(obj); 
      Monitor.Pulse(queue); 
     } 
    } 

    public T Dequeue() { 
     T obj; 
     lock (queue) { 
      while (queue.Count == 0) { 
       Monitor.Wait(queue); 
      } 
      obj = queue.Dequeue(); 
     } 
     return obj; 
    } 
} 

В большинстве любого практического производителя/потребитель сценария вы хотите задушить производитель, поэтому он не может заполнить очередь неограниченной. Посмотрите пример Daxy's BoundedBuffer design. Если вы можете позволить себе переходить на .NET 4.0, то вы, безусловно, хотите воспользоваться своим классом ConcurrentQueue, у него есть еще много черной магии с низкой задержкой и отсрочкой ожидания.

0

Правильный способ просмотра Monitor.Wait и Monitor.Pulse/PulseAll не так служит средством ожидания, а (для Wait) как средство давая знать системе, что код находится в цикле ожидания, который не может выйти пока что что-то интересное не изменится, и (для Pulse/PulseAll) в качестве средства, позволяющего системе знать, что код только что изменил что-то, что может привести к тому, что условие выхода приведет к очередному циклу другого потока. Нужно иметь возможность заменить все вхождения Wait на Sleep(0) и все еще иметь код правильно (даже если он намного менее эффективно, в результате того, что время CPU постоянно меняет условия тестирования, которые не изменились).

Для того чтобы этот механизм работать, необходимо, чтобы избежать возможности следующей последовательности:

  • код в цикле ожидания проверяет состояние, когда оно не выполняется.

  • Код в другой цепочке изменяет состояние так, чтобы оно выполнялось.

  • Код в этом другом потоке блокирует замок (которого еще никто не ждет).

  • Код в цикле ожидания выполняет Wait, так как его состояние не было выполнено.

Wait метод требует, чтобы нить ожидания иметь блокировку, так как это единственный способ, которым это может быть уверено, что состояние его ждет после не изменится между временем это испытанное и временем код выполняет Wait. Метод Pulse требует блокировки, потому что это единственный способ убедиться, что если другой поток сам «совершил» для выполнения Wait, то Pulse не будет происходить до тех пор, пока другой поток на самом деле этого не сделает. Обратите внимание, что использование Wait в замке не гарантирует правильность его использования, но нет способа, чтобы использование Wait вне замка могло быть правильным.

Wait/Pulse дизайн действительно работает достаточно хорошо, если обе стороны сотрудничают. Наибольшие недостатки конструкции IMHO заключаются в следующем: (1) нет механизма для потока, чтобы ждать, пока какой-либо из нескольких объектов не будет пульсирован; (2) даже если один из них «выключает» объект таким образом, что все будущие циклы ожидания должны немедленно выйти (возможно, путем проверки флажка выхода), единственный способ обеспечить, чтобы любой Wait, которому был передан поток, получит Pulse заключается в приобретении замка, возможно, на неопределенный срок, чтобы он стал доступен.

Смежные вопросы