Подтвердить что ты не робот

Синхронизация потоков с монитором и WaitHandle

У меня сложилось впечатление, что после чтения этой статьи лучше использовать Monitor/Lock для синхронизации потоков, поскольку он не использует собственные ресурсы

Конкретная цитата (со страницы 5 статьи):

Monitor.Wait/Pulse - это не единственный способ ждать, когда что-то произойдет в одном потоке и сообщит, что поток, который он произошел в другом. Программисты Win32 долгое время используют различные другие механизмы, и их открывают классы AutoResetEvent, ManualResetEvent и Mutex, все из которых происходят из WaitHandle. Все эти классы находятся в пространстве имен System.Threading. (Механизм Win32 Semaphore не имеет управляемой оболочки в .NET 1.1. Он присутствует в .NET 2.0, но если вам нужно использовать его до этого момента, вы можете либо обернуть его самостоятельно, используя P/Invoke, либо написать собственный счетный семафор класс.)

Некоторые люди могут быть удивлены, узнав, что использование этих классов может быть значительно медленнее, чем использование различных методов Monitor. Я считаю, что это происходит потому, что "выход" управляемого кода в собственные вызовы Win32 и обратно "в" снова дорогой по сравнению с полностью управляемым видом того, что предоставляет Monitor. Читатель также пояснил, что мониторы реализованы в пользовательском режиме, тогда как использование команд ожидания требует переключения в режим ядра, что довольно дорого.

Но, обнаружив SO и прочитав несколько вопросов/ответов, я начал сомневаться в своем понимании того, когда использовать их. Похоже, что многие люди рекомендуют использовать Auto/ManualResetEvent в случаях, когда будет выполняться Monitor.Wait/Pulse. Может ли кто-нибудь объяснить мне, когда синхронизация на основе WaitHandle должна использоваться для мониторинга?

Спасибо

4b9b3361

Ответ 1

Проблема с Monitor.Pulse/Wait заключается в том, что сигнал может потеряться.

Например:

var signal = new ManualResetEvent(false);

// Thread 1
signal.WaitOne();

// Thread 2
signal.Set();

Это всегда будет работать независимо от того, выполняются ли два оператора в разных потоках. Это также очень чистая абстракция и очень четко выражает ваши намерения.

Теперь рассмотрим тот же пример с помощью монитора:

var signal = new object();

// Thread 1
lock (signal)
{
    Monitor.Wait(signal);
}

// Thread 2
lock (signal)
{
    Monitor.Pulse(signal);
}

Здесь сигнал (Pulse) потеряется, если Pulse выполняется до Wait.

Чтобы устранить эту проблему, вам нужно что-то вроде этого:

var signal = new object();
var signalSet = false;

// Thread 1
lock (signal)
{
    while (!signalSet)
    {
        Monitor.Wait(signal);
    }
}

// Thread 2
lock (signal)
{
    signalSet = true;
    Monitor.Pulse(signal);
}

Это работает и, вероятно, еще более совершенным и легким, но менее читаемым. И там, где начинается головная боль, называемая concurrency.

  • Действительно ли этот код работает?
  • В каждом угловом случае?
  • С более чем двумя потоками? (Подсказка: это не так)
  • Как вы его тестируете?

Твердая, надежная, читаемая абстракция часто лучше, чем сырая производительность.

Кроме того, WaitHandles предоставляют некоторые приятные вещи, такие как ожидание набора ручек, которые будут установлены, и т.д. Внедрение этого с помощью мониторов делает головную боль еще хуже...


Общее правило:

  • Используйте Мониторы (lock), чтобы обеспечить эксклюзивный доступ к общему ресурсу
  • Используйте WaitHandles (Manual/AutoResetEvent/Semaphore) для отправки сигналов между потоками

Ответ 2

Я думаю, что у меня неплохой пример 3-го пуля (и хотя этот поток немного стар, он может помочь кому-то).

У меня есть код, в котором поток A получает сетевые сообщения, помещает их в очередь, а затем передает поток B. Потоки B блокируют, деактивируют любые сообщения, разблокируют очередь, затем обрабатывают сообщения.

Проблема заключается в том, что в то время как Thread B обрабатывает и не ждет, если A получает новое сетевое сообщение, завершает и подает импульсы... ну, B не ждет, чтобы импульс просто испарялся. Если B заканчивает то, что он делает, и нажимает на Monitor.Wait(), то недавно добавленное сообщение будет просто зависать, пока не поступит другое сообщение и не будет получен импульс.

Обратите внимание, что эта проблема на самом деле не выглядела некоторое время, так как изначально весь мой цикл был примерно таким:

while (keepgoing)
  { 
  lock (messageQueue)
      {
      while (messageQueue.Count > 0)
          ProcessMessages(messageQueue.DeQueue());

      Monitor.Wait(messageQueue);
      }
  }

Эта проблема не возникала (ну, при отключении были редкие странности, поэтому я немного подозрительно относился к этому коду), пока не решил, что обработка сообщений (потенциально длительная работа) не должна блокировать очередь в виде у него не было причин. Поэтому я изменил его, чтобы удалить сообщения из сообщения, оставьте блокировку, ТОГДА выполните обработку. И тогда мне показалось, что я начал пропускать сообщения, или они придут только после того, как произошло второе событие...

Ответ 3

для случая @Will Gore, рекомендуется всегда продолжать обработку очереди до тех пор, пока она не будет пустой, перед вызовом Monitor.Wait. Например:.

while (keepgoing)
{ 
  List<Message> nextMsgs = new List<Message>();
  lock (messageQueue)
  {
    while (messageQueue.Count == 0)
    {
        try
        {
            Monitor.Wait(messageQueue);
        }
        catch(ThreadInterruptedException)
        {
            //...
        }
    }
    while (messageQueue.Count > 0)
        nextMsgs.Add(messageQueue.DeQueue());
  }
  if(nextMsgs.Count > 0)
    ProcessMessages(nextMsgs);
}

это должно решить проблему, с которой вы столкнулись, и сократить время блокировки (очень важно!).