Синхронизация потоков с монитором и 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 должна использоваться для мониторинга?
Спасибо
Ответы
Ответ 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);
}
это должно решить проблему, с которой вы столкнулись, и сократить время блокировки (очень важно!).