В чем разница между различными вариантами синхронизации потоков в C #?

Может кто-нибудь объяснить разницу между:

  • замок (какой-то объект) {}
  • Использование Mutex
  • Использование семафора
  • Использование монитора
  • Использование других классов синхронизации .Net

Я просто не могу этого понять. Мне кажется, первые два одинаковые?


person user38834    schedule 19.11.2008    source источник
comment
Эта ссылка мне очень помогла: albahari.com/threading   -  person Raphael    schedule 08.01.2014


Ответы (7)


Отличный вопрос. Возможно, я ошибаюсь ... Дайте мне попробовать ... Версия №2 моего исходного ответа ... с немного большим пониманием. Спасибо, что заставили меня прочитать :)

блокировка (объект)

  • это конструкция CLR, предназначенная для (внутриобъектной?) синхронизации потоков. Гарантирует, что только один поток может стать владельцем блокировки объекта и войти в заблокированный блок кода. Другие потоки должны ждать, пока текущий владелец не снимет блокировку, выйдя из блока кода. Также рекомендуется заблокировать закрытый объект-член вашего класса.

Мониторы

  • lock (obj) реализован внутри с помощью монитора. Вы должны предпочесть lock (obj), потому что он предотвращает вас от таких глупостей, как забывание процедуры очистки. Если хотите, это конструкция Monitor для защиты от идиотов.
    Использование Monitor обычно предпочтительнее мьютексов, потому что мониторы были разработаны специально для .NET Framework и поэтому лучше используют ресурсы.

Использование блокировки или монитора полезно для предотвращения одновременного выполнения блоков кода, чувствительных к потокам, но эти конструкции не позволяют одному потоку передавать событие другому. Для этого требуются события синхронизации, которые представляют собой объекты, которые имеют одно из двух состояний, сигнализируемое и несигнальное, которые можно использовать для активации и приостановки потоков. Мьютекс, семафоры - это концепции уровня ОС. например, с именованным мьютексом вы можете синхронизировать несколько (управляемых) exes (гарантируя, что на машине работает только один экземпляр вашего приложения).

Мьютекс:

  • Однако, в отличие от мониторов, мьютекс может использоваться для синхронизации потоков между процессами. Когда он используется для межпроцессной синхронизации, мьютекс называется именованным мьютексом, потому что он должен быть используется в другом приложении, поэтому его нельзя использовать с помощью глобальной или статической переменной. Ему необходимо дать имя, чтобы оба приложения могли получить доступ к одному и тому же объекту мьютекса. Напротив, класс Mutex является оболочкой для конструкции Win32. Хотя он более мощный, чем монитор, мьютекс требует переходов взаимодействия, которые требуют больших вычислительных затрат, чем те, которые требуются классу Monitor.

семафоры (больно мои головной мозг).

  • Используйте класс Semaphore для управления доступом к пулу ресурсов. Потоки входят в семафор, вызывая метод WaitOne, унаследованный от класса WaitHandle, и освобождают семафор, вызывая метод Release. Счетчик семафора уменьшается каждый раз, когда поток входит в семафор, и увеличивается, когда поток освобождает семафор. Когда счетчик равен нулю, последующие запросы блокируются до тех пор, пока другие потоки не освободят семафор. Когда все потоки освободили семафор, счетчик принимает максимальное значение, указанное при создании семафора. Поток может вводить семафор несколько раз ... Класс Semaphore не требует идентификации потока в WaitOne или Release ... программисты обязаны не испортить. Семафоры бывают двух типов: локальные семафоры и названные системные семафоры. Если вы создаете объект семафор с помощью конструктора, который принимает имя, он связывается с семафором операционной системы с этим именем. Именованные системные семафоры видны во всей операционной системе и могут использоваться для синхронизации действий процессы. Локальный семафор существует только в вашем процессе. Он может использоваться любым потоком в вашем процессе, который имеет ссылку на локальный объект Semaphore. Каждый объект семафора представляет собой отдельный локальный семафор.

СТРАНИЦА ДЛЯ ЧТЕНИЯ - Синхронизация потоков (C #)

person Gishu    schedule 19.11.2008
comment
Вы утверждаете, что Monitor не разрешает общение неверно; вы все еще можете Pulse и т. д. с Monitor - person Marc Gravell; 22.07.2009
comment
Ознакомьтесь с альтернативным описанием семафоров - stackoverflow.com/a/40473/968003. Думайте о семафорах как о вышибалах в ночном клубе. В клуб одновременно может входить определенное количество людей. Если клуб заполнен, никто не может войти, но как только один человек уходит, другой может войти. - person Alex Klaus; 13.07.2015

Re "Использование других классов синхронизации .Net" - некоторые из других, о которых вам следует знать:

  • ReaderWriterLock - позволяет нескольким читателям или одному писателю (не в то же время)
  • ReaderWriterLockSlim - как указано выше, меньшие накладные расходы
  • ManualResetEvent - шлюз, пропускающий код при открытии
  • AutoResetEvent - как указано выше, но автоматически закрывается при открытии

В CCR / TPL также есть больше (с низкими накладными расходами) блокирующих конструкций (Parallel Extensions CTP), но IIRC, они будут доступны в .NET 4.0.

person Marc Gravell    schedule 19.11.2008
comment
Итак, если мне нужна простая сигнальная связь (например, завершение асинхронной операции) - мне следует Monitor.Pulse? или использовать SemaphoreSlim или TaskCompletionSource? - person Vivek; 10.01.2014
comment
Используйте TaskCompletionSource для асинхронной операции. По сути, перестаньте думать о потоках и начните думать о задачах (единицах работы). Потоки являются деталью реализации и не имеют отношения к делу. Возвращая TCS, вы можете возвращать результаты, ошибки или обрабатывать отмену, и его легко комбинировать с другими асинхронными операциями (такими как async await или ContinueWith). - person Simon Gillbee; 13.08.2015

Как указано в ECMA и как вы можете видеть из методов Reflected, оператор блокировки в основном эквивалентен

object obj = x;
System.Threading.Monitor.Enter(obj);
try {
   …
}
finally {
   System.Threading.Monitor.Exit(obj);
}

Из вышеупомянутого примера мы видим, что мониторы могут блокировать объекты.

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

Семафоры похожи на мьютексы на стероидах, они разрешают одновременный доступ, обеспечивая максимальное количество одновременных доступов ». Как только предел достигнут, семафор начинает блокировать любой дальнейший доступ к ресурсу, пока один из вызывающих не освободит семафор.

person arul    schedule 19.11.2008
comment
Этот синтаксический сахар был немного изменен в C # 4. Ознакомьтесь с blogs.msdn.com/ericlippert/archive/2009/03/06/ - person Peter Gfader; 18.05.2010

Я сделал классы и поддержку CLR для потоковой передачи в DotGNU, и у меня есть несколько мыслей ...

Если вам не требуются межпроцессные блокировки, вам всегда следует избегать использования мьютексов и семафоров. Эти классы в .NET являются оболочками для Win32 Mutex и Semaphores и имеют довольно большой вес (они требуют переключения контекста в ядро, что является дорогостоящим - особенно если ваша блокировка не оспаривается).

Как уже упоминалось, оператор блокировки C # - это магия компилятора для Monitor.Enter и Monitor.Exit (существующая в try / finally).

У мониторов есть простой, но мощный механизм «сигнал / ожидание», которого нет у мьютексов с помощью методов Monitor.Pulse / Monitor.Wait. Эквивалентом Win32 будут объекты событий через CreateEvent, которые на самом деле также существуют в .NET как WaitHandles. Модель Pulse / Wait похожа на pthread_signal и pthread_wait в Unix, но работает быстрее, потому что они могут быть полностью операциями пользовательского режима в неконкурентном случае.

Monitor.Pulse / Wait прост в использовании. В одном потоке мы блокируем объект, проверяем флаг / состояние / свойство и, если это не то, что мы ожидаем, вызываем Monitor.Wait, который снимает блокировку и ждет, пока не будет отправлен импульс. Когда ожидание возвращается, мы возвращаемся в цикл и снова проверяем флаг / состояние / свойство. В другом потоке мы блокируем объект всякий раз, когда меняем флаг / состояние / свойство, а затем вызываем PulseAll, чтобы разбудить любые прослушивающие потоки.

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

Я не уверен в реализации блокировок Microsoft, но в DotGNU и Mono флаг состояния блокировки хранится в заголовке каждого объекта. Каждый объект в .NET (и Java) может стать блокировкой, поэтому каждый объект должен поддерживать это в своем заголовке. В реализации DotGNU есть флаг, который позволяет вам использовать глобальную хеш-таблицу для каждого объекта, который используется в качестве блокировки - это дает преимущество в устранении 4-байтовых накладных расходов для каждого объекта. Это не очень хорошо для памяти (особенно для встроенных систем, не использующих много потоков), но снижает производительность.

И Mono, и DotGNU эффективно используют мьютексы для выполнения блокировки / ожидания, но используют стиль спин-блокировки compare-and- exchange, чтобы избавиться от необходимости выполнять жесткие блокировки, если в этом нет необходимости:

Вы можете увидеть пример того, как могут быть реализованы мониторы здесь:

http://cvs.savannah.gnu.org/viewvc/dotgnu-pnet/pnet/engine/lib_monitor.c?revision=1.7&view=markup

person tumtumtum    schedule 06.06.2012

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

Добавьте к строковому идентификатору префикс «Global \», чтобы обеспечить надлежащий контроль доступа к общим системным ресурсам. Я просто столкнулся с целой кучей проблем с синхронизацией связи со службой, работающей под учетной записью SYSTEM, прежде чем я понял это.

person nvuono    schedule 06.04.2009

Я бы попытался избежать "lock ()", "Mutex" и "Monitor", если вы можете ...

Ознакомьтесь с новым пространством имен System.Collections.Concurrent в .NET 4
В нем есть несколько хороших поточно-безопасных классов коллекций.

http://msdn.microsoft.com/en-us/library/system.collections.concurrent.aspx

ConcurrentDictionary рок! у меня больше нет ручной блокировки!

person Peter Gfader    schedule 18.05.2010
comment
Избегать блокировки, но использовать монитор? Почему? - person mafu; 13.10.2011
comment
@mafutrct Потому что вам нужно самому позаботиться о синхронизации. - person Peter Gfader; 17.10.2011
comment
О, теперь я понимаю, вы хотели избежать ВСЕХ трех упомянутых идей. Похоже, вы использовали бы Monitor, но не использовали бы блокировку / Mutex. - person mafu; 17.10.2011
comment
Никогда не используйте System.Collections.Concurrent. Они являются основным источником состояний гонки, а также блокируют поток вызывающих объектов. - person Alexander Danilov; 30.01.2020

В большинстве случаев вам не следует использовать блокировки (= мониторы) или мьютексы / семафоры. Все они блокируют ожидающие потоки.

И вам определенно не следует использовать System.Collections.Concurrent классы - они не поддерживают транзакции с несколькими коллекциями, а также используют блокирующую синхронизацию.

Удивительно, но .NET не имеет эффективных механизмов для неблокирующей синхронизации.

Я реализовал последовательную очередь из GCD (Objc/Swift world) на C # - очень легкий, не блокирующий инструмент синхронизации, который использует пул потоков с тестами.

В большинстве случаев это лучший способ синхронизировать что угодно - от доступа к базе данных (привет, sqlite) до бизнес-логики.

person Alexander Danilov    schedule 29.01.2020