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

С#: Монитор - Wait, Pulse, PulseAll

Мне трудно понять Wait(), Pulse(), PulseAll(). Все ли они избегут взаимоблокировки? Буду признателен, если вы объясните, как их использовать?

4b9b3361

Ответ 1

Краткая версия:

lock(obj) {...}

является короткой для Monitor.Enter/Monitor.Exit (с обработкой исключений и т.д.). Если никто не имеет блокировки, вы можете получить его (и запустить свой код) - в противном случае поток будет заблокирован до тех пор, пока блокировка не будет получена (другим потоком, освобождающим его).

Тупик обычно происходит, когда либо A: два потока блокируют вещи в разных порядках:

thread 1: lock(objA) { lock (objB) { ... } }
thread 2: lock(objB) { lock (objA) { ... } }

(здесь, если каждый из них получает первый замок, ни один из них не может получить второй, так как ни один поток не может выйти, чтобы освободить их блокировку)

Этот сценарий можно свести к минимуму, всегда фиксируя его в том же порядке; и вы можете восстановить (до степени) с помощью Monitor.TryEnter (вместо Monitor.Enter/lock) и указать тайм-аут.

или B: вы можете блокировать себя такими вещами, как winforms при переключении потоков при удерживании блокировки:

lock(obj) { // on worker
    this.Invoke((MethodInvoker) delegate { // switch to UI
        lock(obj) { // oopsiee!
            ...
        }
    });
}

Тупик кажется очевидным выше, но это не так очевидно, когда у вас есть код спагетти; Возможные ответы: не переключайте нити при удерживании замков или используйте BeginInvoke, чтобы вы могли, по крайней мере, выйти из блокировки (разрешить воспроизведение пользовательского интерфейса).


Wait/Pulse/PulseAll различны; они предназначены для сигнализации. Я использую этот в этом ответе, чтобы сигнализировать так:

  • Dequeue: если вы пытаетесь удалить данные из очереди, когда очередь пуста, она ждет, пока другой поток добавит данные, которые просыпают заблокированный поток
  • Enqueue: если вы пытаетесь вставлять данные в очередь, когда очередь заполнена, она ожидает, что другой поток удалит данные, которые просыпают заблокированный поток

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

        while (queue.Count >= maxSize)
        {
            Monitor.Wait(queue);
        }

При таком подходе я могу смело добавить другие значения Pulse, без моего существующего кода, предполагая, что "я проснулся, поэтому есть данные" - это удобно, когда (в том же примере) мне позже нужно было добавить a Close().

Ответ 2

Простой рецепт использования Monitor.Wait и Monitor.Pulse. Он состоит из рабочего, босса и телефона, который они используют для общения:

object phone = new object();

Тема "Рабочий":

lock(phone) // Sort of "Turn the phone on while at work"
{
    while(true)
    {
        Monitor.Wait(phone); // Wait for a signal from the boss
        DoWork();
        Monitor.PulseAll(phone); // Signal boss we are done
    }
}

Тема "Босс":

PrepareWork();
lock(phone) // Grab the phone when I have something ready for the worker
{
    Monitor.PulseAll(phone); // Signal worker there is work to do
    Monitor.Wait(phone); // Wait for the work to be done
}

Далее следуют более сложные примеры...

A "Рабочий с чем-то другим":

lock(phone)
{
    while(true)
    {
        if(Monitor.Wait(phone,1000)) // Wait for one second at most
        {
            DoWork();
            Monitor.PulseAll(phone); // Signal boss we are done
        }
        else
            DoSomethingElse();
    }
}

"Нетерпеливый босс":

PrepareWork();
lock(phone)
{
    Monitor.PulseAll(phone); // Signal worker there is work to do
    if(Monitor.Wait(phone,1000)) // Wait for one second at most
        Console.Writeline("Good work!");
}

Ответ 3

Нет, они не защищают вас от тупиков. Это просто более гибкие инструменты для синхронизации потоков. Вот очень хорошее объяснение, как использовать их и очень важный образец использования - без этого шаблона вы сломаете все: http://www.albahari.com/threading/part4.aspx

Ответ 5

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

К сожалению, большая часть работы, необходимой для написания правильного многопоточного кода, в настоящее время является ответственностью разработчиков на С# (и многих других языках). Посмотрите, как F #, Haskell и Clojure обрабатывают это для совершенно другого подхода.

Ответ 6

К сожалению, ни один из Wait(), Pulse() или PulseAll() не обладает магическим свойством, которое вы хотите - это то, что с помощью этого API вы автоматически избегаете тупиковой ситуации.

Рассмотрим следующий код

object incomingMessages = new object(); //signal object

LoopOnMessages()
{
    lock(incomingMessages)
    {
        Monitor.Wait(incomingMessages);
    }
    if (canGrabMessage()) handleMessage();
    // loop
}

ReceiveMessagesAndSignalWaiters()
{
    awaitMessages();
    copyMessagesToReadyArea();
    lock(incomingMessages) {
        Monitor.PulseAll(incomingMessages); //or Monitor.Pulse
    }
    awaitReadyAreaHasFreeSpace();
}

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

Почему?

В конце концов произойдет следующее:

  • Все потребительские потоки выполняют некоторую работу.
  • Приходят сообщения, область готовности не может содержать больше сообщений, и вызывается PulseAll().
  • Ни один пользователь не проснется, потому что никто не ждет
  • Все потребительские потоки называют Wait() [DEADLOCK]

В этом конкретном примере предполагается, что нить производителя никогда не будет вызывать PulseAll() еще раз, потому что у него больше нет места для ввода сообщений. Но есть много и многих сломанных вариантов этого кода. Люди попытаются сделать его более надежным, изменив строку, например, сделав Monitor.Wait(); в

if (!canGrabMessage()) Monitor.Wait(incomingMessages);

К сожалению, этого все еще недостаточно, чтобы исправить это. Чтобы исправить это, вам также необходимо изменить область блокировки, в которой вызывается Monitor.PulseAll():

LoopOnMessages()
{
    lock(incomingMessages)
    {
        if (!canGrabMessage()) Monitor.Wait(incomingMessages);
    }
    if (canGrabMessage()) handleMessage();
    // loop
}

ReceiveMessagesAndSignalWaiters()
{
    awaitMessagesArrive();
    lock(incomingMessages)
    {
        copyMessagesToReadyArea();
        Monitor.PulseAll(incomingMessages); //or Monitor.Pulse
    }
    awaitReadyAreaHasFreeSpace();
}

Ключевым моментом является то, что в фиксированном коде блокировки ограничивают возможные последовательности событий:

  • Потребительские потоки выполняют свою работу и циклы

  • Этот поток получает блокировку

    И благодаря блокировке теперь верно, что либо:

  • а. Сообщения еще не прибыли в область готовности, и он освобождает блокировку, вызвав Wait() ДО ТОГО, что поток приемника сообщений может получить блокировку и скопировать больше сообщений в область готовности или

    б. Сообщения уже поступают в область готовности и получают сообщения INSTEAD OF, вызывающие Wait(). (И хотя он принимает это решение, невозможно, чтобы поток получателя сообщения, например, приобрел блокировку и скопировал больше сообщений в область готовности.)

В результате проблема исходного кода теперь никогда не возникает:  3. Когда вызывается PulseEvent(). Нельзя разбудить потребителя, потому что никто не ждет

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

А также, поскольку вы должны использовать lock (или Monitor.Enter() и т.д.), чтобы использовать Monitor.PulseAll() или Monitor.Wait() без блокировки, вам все равно придется беспокоиться о возможности других взаимоблокировок которые происходят из-за этой блокировки.

Итог: эти API также легко завинчиваются и затормаживаются, т.е. довольно опасны

Ответ 7

Что-то, что меня бросило, - это то, что Pulse просто дает "хедз-ап" в поток в Wait. Поток ожидания не будет продолжаться до тех пор, пока поток, который сделал Pulse, не выдает блокировку, и ожидающий поток успешно выигрывает.

lock(phone) // Grab the phone
{
    Monitor.PulseAll(phone); // Signal worker
    Monitor.Wait(phone); // ****** The lock on phone has been given up! ******
}

или

lock(phone) // Grab the phone when I have something ready for the worker
{
    Monitor.PulseAll(phone); // Signal worker there is work to do
    DoMoreWork();
} // ****** The lock on phone has been given up! ******

В обоих случаях это происходит только до тех пор, пока "блокировка на телефоне не будет удалена", которую может получить другой поток.

Там могут быть другие потоки, ожидающие блокировки от Monitor.Wait(phone) или lock(phone). Только тот, кто выигрывает блокировку, будет продолжать.