Требуется ли синхронизация Monitor.Wait?

Я разработал общую очередь производителей-потребителей, которая пульсирует монитором следующим образом:

очередь:

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

очередь:

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?


person user437631    schedule 26.09.2010    source источник


Ответы (3)


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

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

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

person Jon Skeet    schedule 26.09.2010
comment
почему вы предпочитаете это? как бы вы синхронизировали монитор? просто заблокируйте объект шкафчика, используемый для монитора? Разве блокировка не добавляет еще одно переключение контекста, которое не нужно ResetEvents? - person user437631; 26.09.2010
comment
@ user437631: Да, обычный оператор lock подойдет. Это может потребовать или не потребовать дополнительного переключения контекста - и я не думаю, что у вас есть какие-либо доказательства того, что ResetEvents не потребует этого. На самом деле, поскольку они являются внутренними объектами CLR, а не объектами Win32, которые потенциально могут использоваться совместно, мониторы требуют меньшего ожидания, чем ResetEvents. - person Jon Skeet; 26.09.2010

Из описания 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;
    }
}

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

person Hans Passant    schedule 26.09.2010

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

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

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

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

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

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

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

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

person supercat    schedule 08.05.2013