Допустим, у класса есть public int counter
поле, доступ к которому осуществляется несколькими потоками. Это int
только увеличивается или уменьшается.
Чтобы увеличить это поле, какой подход следует использовать и почему?
lock(this.locker) this.counter++;
,Interlocked.Increment(ref this.counter);
,- Измените модификатор доступа
counter
наpublic volatile
.
Теперь, когда я обнаружил volatile
, я удалил много lock
утверждений и использование Interlocked
. Но есть ли причина не делать этого?
Ответы:
Худший (на самом деле не будет работать)
Как уже упоминали другие люди, это само по себе вообще не безопасно. Дело в
volatile
том, что несколько потоков, работающих на нескольких процессорах, могут и будут кешировать данные и изменять порядок команд.Если это не так
volatile
, и ЦП A увеличивает значение, то ЦП B может фактически не увидеть это увеличенное значение до некоторого времени спустя, что может вызвать проблемы.Если это так
volatile
, то это гарантирует, что два процессора одновременно видят одни и те же данные. Это не мешает им чередовать операции чтения и записи, и это та проблема, которую вы пытаетесь избежать.Второе место:
Это безопасно (при условии, что вы помните,
lock
где бы вы ни находилисьthis.counter
). Это препятствует тому, чтобы любые другие потоки выполнили любой другой код, который охраняетсяlocker
. Использование блокировок также предотвращает проблемы переупорядочения многопроцессорных систем, как описано выше, и это здорово.Проблема в том, что блокировка медленная, и если вы повторно используете ее
locker
в каком-то другом месте, которое на самом деле не связано, то вы можете в конечном итоге заблокировать другие потоки без причины.Лучший
Это безопасно, поскольку эффективно выполняет чтение, приращение и запись в одно нажатие, которое не может быть прервано. Из-за этого это не повлияет на любой другой код, и вам также не нужно забывать блокировать в другом месте. Это также очень быстро (как говорит MSDN, на современных ЦП это часто буквально одна инструкция ЦП).
Я не совсем уверен, однако, если это обойти другие процессоры, переупорядочивающие вещи, или вам также нужно объединить volatile с приращением.InterlockedNotes:
Сноска: Что на самом деле хорошо для изменчивого?
Как
volatile
не предотвратить такие проблемы многопоточности, для чего это нужно? Хорошим примером является то, что у вас есть два потока, один из которых всегда записывает в переменную (скажемqueueLength
), а другой всегда читает из этой же переменной.Если
queueLength
он не является энергозависимым, поток A может записывать пять раз, но поток B может видеть, что эти записи задерживаются (или даже могут быть в неправильном порядке).Решением будет блокировка, но вы также можете использовать volatile в этой ситуации. Это гарантирует, что поток B всегда будет видеть самую последнюю информацию, которую написал поток A. Однако обратите внимание, что эта логика работает, только если у вас есть писатели, которые никогда не читают, и читатели, которые никогда не пишут, и если то, что вы пишете, является атомарной ценностью. Как только вы выполните одну операцию чтения-изменения-записи, вам нужно перейти к блокированным операциям или использовать блокировку.
источник
РЕДАКТИРОВАТЬ: Как отмечено в комментариях, в эти дни я рад использовать
Interlocked
для случаев одной переменной, где это, очевидно, хорошо. Когда все станет сложнее, я все равно вернусь к блокировке ...Использование
volatile
не поможет, когда вам нужно увеличить - потому что чтение и запись - это отдельные инструкции. Другой поток может изменить значение после того, как вы прочитали, но перед тем, как написать обратно.Лично я почти всегда просто блокируюсь - легче сделать правильный путь, который, очевидно, прав, чем волатильность или взаимосвязь. Инкремент. Насколько я понимаю, многопоточность без блокировок предназначена для настоящих экспертов по многопоточности, из которых я не один. Если Джо Даффи и его команда создают хорошие библиотеки, которые будут распараллеливать вещи без такой большой блокировки, как то, что я бы построил, это невероятно, и я буду использовать это в одно мгновение - но когда я сам делаю потоки, я пытаюсь будь проще.
источник
«
volatile
» не заменяетInterlocked.Increment
! Это просто гарантирует, что переменная не кэшируется, а используется напрямую.Инкремент переменной требует фактически трех операций:
Interlocked.Increment
выполняет все три части как одну атомарную операцию.источник
volatile
это не убедитесь , что переменная не кэшируется. Это просто накладывает ограничения на то, как его можно кэшировать. Например, он все еще может быть кэширован в кеше L2 ЦП, потому что он сделан аппаратно согласованным. Это все еще может быть предпочтительным. Записи все еще можно публиковать в кеш и тд. (Что, я думаю, было тем, к чему добился Зак.)Либо блокировка, либо инкрементное приращение - это то, что вы ищете.
Volatile определенно не то, что вам нужно - она просто указывает компилятору обрабатывать переменную как всегда изменяющуюся, даже если текущий путь кода позволяет компилятору оптимизировать чтение из памяти в противном случае.
например
если 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 ,
источник
Блокированные функции не блокируются. Они являются атомарными, что означает, что они могут завершаться без возможности переключения контекста во время приращения. Так что шансов зайти в тупик или ждать нет.
Я бы сказал, что вы всегда должны отдавать предпочтение блокировке и приращению.
Volatile полезно, если вам нужно, чтобы записи в одном потоке читались в другом, и если вы хотите, чтобы оптимизатор не переупорядочивал операции с переменной (потому что события происходят в другом потоке, о котором оптимизатор не знает). Это ортогональный выбор того, как вы увеличиваете.
Это действительно хорошая статья, если вы хотите больше узнать о коде без блокировки и о том, как правильно его написать.
http://www.ddj.com/hpc-high-performance-computing/210604448
источник
Блокировка (...) работает, но может блокировать поток и может вызвать взаимоблокировку, если другой код использует такие же блокировки несовместимым способом.
Interlocked. * - это правильный способ сделать это ... гораздо меньше накладных расходов, поскольку современные процессоры поддерживают это как примитив.
изменчиво само по себе не правильно. Поток, пытающийся получить и затем записать измененное значение, все еще может конфликтовать с другим потоком, делающим то же самое.
источник
Я провел некоторый тест, чтобы увидеть, как на самом деле работает теория: kennethxu.blogspot.com/2009/05/interlocked-vs-monitor-performance.html . Мой тест был больше сфокусирован на CompareExchnage, но результат для Increment аналогичен. Блокировка не требуется быстрее в среде с несколькими процессорами. Вот результат теста для увеличения на 16-летнем сервере с 2-мя процессорами. Имейте в виду, что тест также включает безопасное чтение после увеличения, что типично для реального мира.
источник
Я второй ответ Джона Скита и хочу добавить следующие ссылки для всех, кто хочет узнать больше о "volatile" и Interlocked:
Атомность, изменчивость и неизменность различны, часть первая («Сказочные приключения Эрика Липперта в кодировании»)
Атомность, изменчивость и неизменность различны, часть вторая
Атомность, изменчивость и неизменность различны, часть третья
Sayonara Volatile - (снимок Wayback Machine из блога Джо Даффи, как он появился в 2012 году)
источник
Я хотел бы добавить к упомянуты в других ответах разницы между
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
оператора.источник