Летучий против Блокированный против блокировки

672

Допустим, у класса есть public int counterполе, доступ к которому осуществляется несколькими потоками. Это intтолько увеличивается или уменьшается.

Чтобы увеличить это поле, какой подход следует использовать и почему?

  • lock(this.locker) this.counter++;,
  • Interlocked.Increment(ref this.counter);,
  • Измените модификатор доступа counterна public volatile.

Теперь, когда я обнаружил volatile, я удалил много lockутверждений и использование Interlocked. Но есть ли причина не делать этого?

ядро
источник
Прочитайте ссылку на Threading в C # . Он охватывает все тонкости вашего вопроса. Каждый из трех имеет разные цели и побочные эффекты.
Спулсон
1
simple-talk.com/blogs/2012/01/24/… вы можете увидеть использование volitable в массивах, я не совсем понимаю, но это еще одна ссылка на то, что это делает.
Эран Отзап 28.09.13
50
Это все равно что сказать: «Я обнаружил, что спринклерная система никогда не активируется, поэтому я собираюсь удалить ее и заменить на дымовую сигнализацию». Причина этого не в том, что это невероятно опасно и практически не приносит пользы . Если у вас есть время потратить на изменение кода, найдите способ сделать его менее многопоточным ! Не находите способ сделать многопоточный код более опасным и легко взломанным!
Эрик Липперт
1
В моем доме есть как дождеватели, так и спринклеры . При увеличении счетчика в одном потоке и чтении его в другом кажется, что вам нужны и блокировка (или блокировка), и ключевое слово volatile. Правда?
Йойо
2
@yoyo Нет, вам не нужны оба.
Дэвид Шварц

Ответы:

860

Худший (на самом деле не будет работать)

Измените модификатор доступа counterнаpublic volatile

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

Если это не так volatile , и ЦП A увеличивает значение, то ЦП B может фактически не увидеть это увеличенное значение до некоторого времени спустя, что может вызвать проблемы.

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

Второе место:

lock(this.locker) this.counter++;

Это безопасно (при условии, что вы помните, lockгде бы вы ни находились this.counter). Это препятствует тому, чтобы любые другие потоки выполнили любой другой код, который охраняется locker. Использование блокировок также предотвращает проблемы переупорядочения многопроцессорных систем, как описано выше, и это здорово.

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

Лучший

Interlocked.Increment(ref this.counter);

Это безопасно, поскольку эффективно выполняет чтение, приращение и запись в одно нажатие, которое не может быть прервано. Из-за этого это не повлияет на любой другой код, и вам также не нужно забывать блокировать в другом месте. Это также очень быстро (как говорит MSDN, на современных ЦП это часто буквально одна инструкция ЦП).

Я не совсем уверен, однако, если это обойти другие процессоры, переупорядочивающие вещи, или вам также нужно объединить volatile с приращением.

InterlockedNotes:

  1. Взаимосвязанные методы безопасны одновременно на любом количестве ядер или процессоров.
  2. Взаимосвязанные методы применяют полную границу вокруг выполняемых инструкций, поэтому изменение порядка не происходит.
  3. Методы с блокировкой не нуждаются или даже не поддерживают доступ к изменчивому полю , поскольку volatile размещает половину ограждения вокруг операций на заданном поле, а блокированное использует полное ограждение.

Сноска: Что на самом деле хорошо для изменчивого?

Как volatileне предотвратить такие проблемы многопоточности, для чего это нужно? Хорошим примером является то, что у вас есть два потока, один из которых всегда записывает в переменную (скажем queueLength), а другой всегда читает из этой же переменной.

Если queueLengthон не является энергозависимым, поток A может записывать пять раз, но поток B может видеть, что эти записи задерживаются (или даже могут быть в неправильном порядке).

Решением будет блокировка, но вы также можете использовать volatile в этой ситуации. Это гарантирует, что поток B всегда будет видеть самую последнюю информацию, которую написал поток A. Однако обратите внимание, что эта логика работает, только если у вас есть писатели, которые никогда не читают, и читатели, которые никогда не пишут, и если то, что вы пишете, является атомарной ценностью. Как только вы выполните одну операцию чтения-изменения-записи, вам нужно перейти к блокированным операциям или использовать блокировку.

Орион Эдвардс
источник
29
«Я не совсем уверен ... нужно ли вам комбинировать энергозависимость с приращением». Они не могут быть объединены AFAIK, так как мы не можем передать изменчивый реф. Отличный ответ, кстати.
Хосам Али
41
Большое спасибо! Ваша сноска на тему «Что на самом деле хорошо для volatile» - это то, что я искал и подтвердил, как я хочу использовать volatile.
Жак Бош
7
Другими словами, если var объявлен как volatile, компилятор будет предполагать, что значение var не будет оставаться тем же самым (то есть volatile) каждый раз, когда ваш код сталкивается с ним. Таким образом, в цикле, таком как: while (m_Var) {} и m_Var установлен в false в другом потоке, компилятор не просто проверяет, что уже находится в регистре, который был ранее загружен значением m_Var, но считывает значение из m_Var снова. Однако это не означает, что отсутствие объявления volatile приведет к бесконечному циклу - указание volatile гарантирует, что этого не произойдет, если для m_Var установлено значение false в другом потоке.
Зак видел
35
@ Zach Saw: Под моделью памяти для C ++, volatile - это то, как вы это описали (в основном полезно для отображаемой на устройстве памяти, а не для чего-то еще). Под моделью памяти для CLR (этот вопрос помечен C #) является то, что volatile будет вставлять барьеры памяти вокруг чтения и записи в это место хранения. Барьеры памяти (и специальные запертые варианты некоторых инструкций по сборке) - вы говорите процессору не менять порядок вещей, и они довольно важны ...
Орион Эдвардс
19
@ZachSaw: изменчивое поле в C # не позволяет компилятору C # и jit выполнять определенные оптимизации, которые бы кэшировали значение. Это также дает определенные гарантии относительно того, какой порядок чтения и записи может наблюдаться в нескольких потоках. В качестве детали реализации это может быть сделано путем введения барьеров памяти при чтении и записи. Точная семантика гарантирована, описаны в спецификации; обратите внимание, что спецификация не гарантирует, что последовательный порядок всех изменчивых операций записи и чтения будет наблюдаться всеми потоками.
Эрик Липперт
147

РЕДАКТИРОВАТЬ: Как отмечено в комментариях, в эти дни я рад использовать Interlockedдля случаев одной переменной, где это, очевидно, хорошо. Когда все станет сложнее, я все равно вернусь к блокировке ...

Использование volatileне поможет, когда вам нужно увеличить - потому что чтение и запись - это отдельные инструкции. Другой поток может изменить значение после того, как вы прочитали, но перед тем, как написать обратно.

Лично я почти всегда просто блокируюсь - легче сделать правильный путь, который, очевидно, прав, чем волатильность или взаимосвязь. Инкремент. Насколько я понимаю, многопоточность без блокировок предназначена для настоящих экспертов по многопоточности, из которых я не один. Если Джо Даффи и его команда создают хорошие библиотеки, которые будут распараллеливать вещи без такой большой блокировки, как то, что я бы построил, это невероятно, и я буду использовать это в одно мгновение - но когда я сам делаю потоки, я пытаюсь будь проще.

Джон Скит
источник
16
+1 за то, что теперь я могу забыть о кодировании без блокировки.
Xaqron
5
Коды без блокировок определенно не являются по-настоящему свободными от блокировок, поскольку они блокируются на каком-то этапе - будь то на уровне шины (FSB) или на уровне межпроцессорного процессора, все равно существует штраф, который вам придется заплатить. Однако блокировка на этих более низких уровнях, как правило, быстрее, если вы не насыщаете полосу пропускания того места, где происходит блокировка.
Зак видел
2
В Interlocked нет ничего плохого, это именно то, что вы ищете, и быстрее, чем полная блокировка ()
Jaap
5
@Jaap: Да, в эти дни я бы использовал блокировку для подлинного единого счетчика. Я просто не хотел бы начинать бездельничать, пытаясь отработать взаимодействие между несколькими обновлениями переменных без блокировки.
Джон Скит
6
@ZachSaw: Ваш второй комментарий говорит, что блокированные операции «блокируются» на некотором этапе; термин «блокировка» обычно подразумевает, что одна задача может поддерживать исключительный контроль над ресурсом в течение неограниченного промежутка времени; Основное преимущество программирования без блокировок состоит в том, что оно предотвращает опасность того, что ресурс станет непригодным для использования в результате того, что задача владельца будет отложена. Синхронизация шины, используемая взаимосвязанным классом, не просто «в целом быстрее» - в большинстве систем она имеет ограниченное время наихудшего случая, тогда как блокировки - нет.
суперкат
44

« volatile» не заменяет Interlocked.Increment! Это просто гарантирует, что переменная не кэшируется, а используется напрямую.

Инкремент переменной требует фактически трех операций:

  1. читать
  2. приращение
  3. записывать

Interlocked.Increment выполняет все три части как одну атомарную операцию.

Михаил Даматов
источник
4
Иными словами, заблокированные изменения полностью защищены и поэтому являются атомарными. Летучие члены защищены только частично и поэтому не гарантируют, что они являются поточно-ориентированными.
JoeGeeky
1
На самом деле, volatileэто не убедитесь , что переменная не кэшируется. Это просто накладывает ограничения на то, как его можно кэшировать. Например, он все еще может быть кэширован в кеше L2 ЦП, потому что он сделан аппаратно согласованным. Это все еще может быть предпочтительным. Записи все еще можно публиковать в кеш и тд. (Что, я думаю, было тем, к чему добился Зак.)
Дэвид Шварц
42

Либо блокировка, либо инкрементное приращение - это то, что вы ищете.

Volatile определенно не то, что вам нужно - она ​​просто указывает компилятору обрабатывать переменную как всегда изменяющуюся, даже если текущий путь кода позволяет компилятору оптимизировать чтение из памяти в противном случае.

например

while (m_Var)
{ }

если m_Var установлен в false в другом потоке, но он не объявлен как volatile, компилятор может сделать его бесконечным циклом (но это не значит, что так будет всегда), проверяя регистр процессора (например, EAX, потому что это было то, что m_Var было извлечено с самого начала) вместо того, чтобы выдавать другое чтение в область памяти m_Var (это может быть кэшировано - мы не знаем и не заботимся, и в этом суть когерентности кэша x86 / x64). Все ранее опубликованные сообщения, в которых упоминалось переупорядочение инструкций, просто показывают, что они не понимают архитектуры x86 / x64. Летучий невыпускать барьеры для чтения / записи, как это подразумевается в предыдущих постах, в которых говорится: «это предотвращает переупорядочение». Фактически, еще раз благодаря протоколу MESI, мы гарантируем, что результат, который мы читаем, всегда одинаков для всех процессоров, независимо от того, были ли фактические результаты перенесены в физическую память или просто находятся в кеше локального процессора. Я не буду вдаваться в подробности этого, но будьте уверены, что если это пойдет не так, Intel / AMD, скорее всего, выдаст отзыв процессора! Это также означает, что нам не нужно заботиться о неисполнении заказа и т. Д. Результаты всегда гарантированно удаляются по порядку - иначе мы забиты!

При использовании Interlocked Increment процессор должен выйти, извлечь значение по указанному адресу, затем увеличить его и записать обратно - все это при исключительном владении всей строкой кэша (lock xadd), чтобы убедиться, что другие процессоры не могут изменить его ценность.

С volatile у вас все равно останется всего одна инструкция (при условии, что JIT эффективен, как и должен) - inc dword ptr [m_Var]. Тем не менее, процессор (cpuA) не запрашивает монопольного владения строкой кэша, в то время как делает все, что делал с заблокированной версией. Как вы можете себе представить, это означает, что другие процессоры могут записать обновленное значение обратно в m_Var после того, как оно было прочитано cpuA. Таким образом, вместо того, чтобы теперь увеличивать значение в два раза, вы получите только один раз.

Надеюсь, что это проясняет проблему.

Дополнительную информацию см. В разделе «Понимание влияния методов низкого уровня блокировки в многопоточных приложениях» - http://msdn.microsoft.com/en-au/magazine/cc163715.aspx.

ps что побудило этот очень поздний ответ? Все ответы были настолько явно неверны (особенно тот, который помечен как ответ) в их объяснении, я просто должен был уточнить это для всех, кто читает это. пожимает

pps Я предполагаю, что целью является x86 / x64, а не IA64 (у него другая модель памяти). Обратите внимание на то, что спецификации Microsoft ECMA испорчены тем, что в них указывается самая слабая модель памяти, а не самая сильная (всегда лучше указывать против самой сильной модели памяти, чтобы она была согласованной на разных платформах - в противном случае код, который будет работать 24-7 на x86 / x64 может не работать вообще на IA64, хотя Intel внедрила аналогичную модель памяти для IA64) - Microsoft признала это самостоятельно - http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx ,

Зак Со
источник
3
Интересно. Можете ли вы сослаться на это? Я бы с радостью проголосовал за это, но публикация на каком-то агрессивном языке через 3 года после высоко оцененного ответа, соответствующего прочитанным мною ресурсам, потребует чуть более ощутимых доказательств.
Стивен Эверс
Если вы можете указать, на какую часть вы хотите сослаться, я был бы рад выкопать кое-что откуда-то (я очень сомневаюсь, что отдал какие-либо торговые секреты поставщика x86 / x64, так что они должны быть легко доступны из вики, Intel PRMs (справочники программиста), блоги MSFT, MSDN или что-то подобное) ...
Зак Со
2
Почему кто-то захочет предотвратить кеширование процессора, я не знаю. Вся недвижимость (определенно немаловажная по размеру и стоимости), предназначенная для выполнения когерентности кэша, полностью теряется, если это так ... Если вы не требуете никакой когерентности кэша, такой как графическая карта, устройство PCI и т. Д., Вы бы не настроили строка кэша для сквозной записи.
Зак видел
4
Да, все, что вы говорите, если не 100%, по крайней мере, 99% на марке. Этот сайт (в основном) довольно полезен, когда вы находитесь на пороге разработки на работе, но, к сожалению, точность ответов, соответствующих (игре) голосов, отсутствует. Таким образом, в основном в stackoverflow вы можете получить представление о том, что является популярным пониманием читателей, а не тем, чем оно является на самом деле. Иногда лучшие ответы - просто бред, мифы. И, к сожалению, это то, что размножается на людей, которые сталкиваются с чтением при решении проблемы. Это понятно, хотя никто не может знать все.
user1416420 14.12.12
1
@BenVoigt Я мог бы продолжить и ответить на все архитектуры, на которых работает .NET, но это заняло бы несколько страниц и определенно не подходит для SO. Гораздо лучше обучать людей на основе наиболее широко используемой аппаратной мем-модели .NET, чем произвольной. И с моими комментариями «везде» я исправлял ошибки, которые допускали люди, предполагая очистку / аннулирование кэша и т. Д. Они делали предположения о базовом оборудовании, не указывая, какое именно оборудование.
Зак видел
16

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

Я бы сказал, что вы всегда должны отдавать предпочтение блокировке и приращению.

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

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

http://www.ddj.com/hpc-high-performance-computing/210604448

Лу Франко
источник
11

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

Interlocked. * - это правильный способ сделать это ... гораздо меньше накладных расходов, поскольку современные процессоры поддерживают это как примитив.

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

Роб Уокер
источник
8

Я провел некоторый тест, чтобы увидеть, как на самом деле работает теория: kennethxu.blogspot.com/2009/05/interlocked-vs-monitor-performance.html . Мой тест был больше сфокусирован на CompareExchnage, но результат для Increment аналогичен. Блокировка не требуется быстрее в среде с несколькими процессорами. Вот результат теста для увеличения на 16-летнем сервере с 2-мя процессорами. Имейте в виду, что тест также включает безопасное чтение после увеличения, что типично для реального мира.

D:\>InterlockVsMonitor.exe 16
Using 16 threads:
          InterlockAtomic.RunIncrement         (ns):   8355 Average,   8302 Minimal,   8409 Maxmial
    MonitorVolatileAtomic.RunIncrement         (ns):   7077 Average,   6843 Minimal,   7243 Maxmial

D:\>InterlockVsMonitor.exe 4
Using 4 threads:
          InterlockAtomic.RunIncrement         (ns):   4319 Average,   4319 Minimal,   4321 Maxmial
    MonitorVolatileAtomic.RunIncrement         (ns):    933 Average,    802 Minimal,   1018 Maxmial
Кеннет Сюй
источник
Однако пример кода, который вы тестировали, был тривиален - на самом деле нет смысла тестировать его таким образом! Лучше всего было бы понять, что на самом деле делают разные методы, и использовать подходящий, исходя из имеющегося у вас сценария использования.
Зак видел
@ Зач, как здесь обсуждали сценарий увеличения счетчика потокобезопасным способом. Какой еще сценарий использования был у вас на уме или как бы вы его протестировали? Спасибо за комментарий Кстати.
Кеннет Сюй
Дело в том, что это искусственный тест. Вы не собираетесь забивать то же место, что часто в любом сценарии реального мира. Если да, то ФСБ в узком месте (как показано на ваших серверах). В любом случае, посмотрите на мой ответ в своем блоге.
Зак видел
2
Оглядываясь назад. Если истинное узкое место связано с FSB, то реализация монитора должна соблюдать то же самое узкое место. Реальная разница заключается в том, что Interlocked занята ожиданием и повторением, что становится реальной проблемой при высокой производительности подсчета. По крайней мере, я надеюсь, что мой комментарий привлечет внимание к тому, что Interlocked не всегда является правильным выбором для подсчета. Тот факт, что люди смотрят на альтернативы, хорошо объяснил это. Вам нужен длинный сумматор gee.cs.oswego.edu/dl/jsr166/dist/jsr166edocs/jsr166e/…
Кеннет Сюй
8

Я второй ответ Джона Скита и хочу добавить следующие ссылки для всех, кто хочет узнать больше о "volatile" и Interlocked:

Атомность, изменчивость и неизменность различны, часть первая («Сказочные приключения Эрика Липперта в кодировании»)

Атомность, изменчивость и неизменность различны, часть вторая

Атомность, изменчивость и неизменность различны, часть третья

Sayonara Volatile - (снимок Wayback Machine из блога Джо Даффи, как он появился в 2012 году)

zihotki
источник
3

Я хотел бы добавить к упомянуты в других ответах разницы между volatile, Interlockedи lock:

Ключевое слово volatile может применяться к полям следующих типов :

  • Типы ссылок.
  • Типы указателей (в небезопасном контексте). Обратите внимание, что, хотя сам указатель может быть изменчивым, объект, на который он указывает, не может. Другими словами, вы не можете объявить «указатель» как «изменчивый».
  • Простые типы , такие как sbyte, byte, short, ushort, int, uint, char, float, и bool.
  • Перечисление с одним из следующих базовых типов: byte, sbyte, short, USHORT, intили uint.
  • Параметры общего типа, как известно, являются ссылочными типами.
  • IntPtrи UIntPtr.

Другие типы , включая doubleи long, не могут быть помечены как «volatile», потому что чтение и запись в поля этих типов не могут быть гарантированно атомарными. Чтобы защитить многопоточный доступ к этим типам полей, используйте члены Interlockedкласса или защитите доступ с помощью lockоператора.

Вадим С.
источник