Летучие и блокировкой и замком
допустим, что класс имеет public int counter
поле, к которому обращаются несколько потоков. Это int
увеличивается или уменьшается.
чтобы увеличить это поле, какой подход следует использовать и почему?
-
lock(this.locker) this.counter++;
, -
Interlocked.Increment(ref this.counter);
, - изменить модификатор доступа
counter
доpublic volatile
.
теперь, когда я обнаружил volatile
, Я удаляю много lock
заявления и использование Interlocked
. Но есть ли причина не делать этого?
9 ответов:
худший (на самом деле не будет работать)
изменить модификатор доступа
counter
доpublic volatile
как уже упоминали другие люди, это само по себе на самом деле не безопасно вообще. Суть
volatile
это то, что несколько потоков, работающих на нескольких процессорах, могут и будут кэшировать данные и переупорядочивать инструкции.если это не
volatile
, и CPU a увеличивает значение, тогда CPU B может фактически не видеть это увеличенное значение до некоторого времени позже, что может вызвать проблемы.если это
volatile
, это обеспечивает два процессора видят одни и те же данные одновременно. Это не мешает им вообще чередовать свои операции чтения и записи, что является проблемой, которую вы пытаетесь избежать.Второе Место:
lock(this.locker) this.counter++
;это можно сделать (при условии, что вы помните, чтобы
lock
везде, где вы получаете доступthis.counter
). Это предотвращает любые другие потоки выполнения любого другого кода, который охраняетсяlocker
. Использование блокировок также предотвращает проблемы с переупорядочением нескольких процессоров, как указано выше, что отлично.проблема в том, что блокировка происходит медленно, и если вы повторно используете
locker
в каком-то другом месте, которое на самом деле не связано, тогда вы можете в конечном итоге блокировать другие потоки без причины.лучшие
Interlocked.Increment(ref this.counter);
это безопасно, так как он эффективно делает чтение, инкремент, и написать в "один удар", который не может быть прерван. Из-за этого он не повлияет ни на какой другой код, и вам не нужно помнить, чтобы заблокировать в другом месте. Это также очень быстро (как говорит MSDN, на современных процессорах это часто буквально одна инструкция CPU).
я не совсем уверен, однако, если он обходит другие процессоры, переупорядочивая вещи, или если вам также нужно сочетать volatile с прирост.InterlockedNotes:
- Блокированные методы одновременно безопасны на любом количестве ядер или процессоров.
- Блокированные методы применяют полный забор вокруг инструкций, которые они выполняют, поэтому переупорядочения не происходит.
- блокировать методов не нужно или даже не поддерживает доступ к изменчивому полю, по мере того как испаряющий помещен половинной загородке вокруг деятельностей на, котор дали поле и блокировать использует полный забор.
сноска: то, что изменчиво на самом деле хорошо.
как
volatile
не предотвращает такие проблемы многопоточности, для чего это? Хорошим примером является то, что у вас есть два потока, один из которых всегда записывает в переменную (скажемqueueLength
), и тот, который всегда читает от той же переменной.если
queueLength
не является изменчивым, поток A может писать пять раз, но поток B может видеть, что эти записи задерживаются (или даже потенциально в неправильный порядок).решением было бы заблокировать, но вы также можете использовать volatile в этой ситуации. Это гарантирует, что поток B всегда будет видеть самую последнюю вещь, которую написал поток A. Обратите внимание, однако, что эта логика только работает, если у вас есть писатели, которые никогда не читают, и читатели, которые никогда не пишут,и если то, что вы пишете, является атомарным значением. Как только вы сделаете одно чтение-изменение-запись, вам нужно перейти к взаимосвязанным операциям или используйте замок.
EDIT: как отмечено в комментариях, в эти дни я с удовольствием использую
Interlocked
для случаев a одной переменной где очевидно ОК. Когда все усложнится, я все равно вернусь к блокировке...используя
volatile
не поможет, когда вам нужно увеличить - потому что чтение и запись являются отдельными инструкциями. Другой поток может изменить значение после того, как вы читали, но прежде чем писать туда.лично я почти всегда просто блокировка - это легче получить право в пути, который очевидно право чем или волатильность или блокировать.Прирост. Насколько я могу судить, многопоточность без блокировки-это для настоящих экспертов по потокам, из которых я не один. Если Джо Даффи и его команда создадут хорошие библиотеки, которые будут распараллеливать вещи без такой блокировки, как то, что я бы построил, это потрясающе, и я буду использовать его в мгновение ока, но когда я делаю поток сам, я стараюсь держать его простым.
"
volatile
" не заменитьInterlocked.Increment
! Он просто гарантирует, что переменная не кэшируется, а используется напрямую.увеличение переменной требует фактически трех операций:
- читать
- инкремент
- написать
Interlocked.Increment
выполняет все три части как одну атомарную операцию.
либо блокировка, либо блокированное приращение-это то, что вы ищете.
Volatile определенно не то, что вам нужно - он просто говорит компилятору обрабатывать переменную как всегда меняющуюся, даже если текущий путь кода позволяет компилятору оптимизировать чтение из памяти в противном случае.
например
while (m_Var) { }
если m_Var имеет значение false в другом потоке, но он не объявлен как volatile, компилятор может сделать его бесконечным циклом (но это не значит всегда будет), заставив его проверить регистр процессора (например, EAX, потому что это было то, что m_var был выбран с самого начала) Вместо того, чтобы выдавать другое чтение в ячейку памяти m_Var (это может быть кэшировано - мы не знаем и не заботимся, и это точка согласованности кэша x86/x64). Все сообщения ранее других, которые упоминали о переупорядочении инструкций, просто показывают, что они не понимают архитектуры x86/x64. Летучие делает не барьеры чтения/записи вопроса как подразумевается, что более ранние сообщения говорят: "это предотвращает переупорядочение". На самом деле, благодаря протоколу MESI, нам гарантируется, что результат, который мы читаем, всегда одинаков для всех процессоров независимо от того, были ли фактические результаты удалены в физическую память или просто находятся в кэше локального процессора. Я не буду вдаваться в подробности этого, но будьте уверены, что если это пойдет не так, Intel/AMD, скорее всего, выпустит отзыв процессора! Это также означает, что нам не нужно заботиться о выполнении заказов так далее. Результаты всегда гарантированно уходят на пенсию по порядку-иначе мы набиты!
с блокированным приращением процессор должен выйти, извлечь значение из заданного адреса, затем увеличить и записать его обратно-все это, имея исключительное право собственности на всю строку кэша (lock xadd), чтобы убедиться, что никакие другие процессоры не могут изменить его значение.
С volatile вы все равно получите только 1 инструкцию (предполагая, что JIT эффективен, как и должен) - inc dword ptr [m_Var]. Однако процессор (cpuA) не запрашивает исключительное право собственности на строку кэша, делая все, что он сделал с блокированной версией. Как вы можете себе представить, это означает, что другие процессоры могут записать обновленное значение обратно в m_Var после его чтения cpuA. Поэтому вместо того, чтобы Теперь увеличить значение в два раза, вы получите только один раз.
надеюсь, что это проясняет вопрос.
для получения дополнительной информации см. раздел " понимание влияния методов низкой блокировки в Многопоточные приложения' -http://msdn.microsoft.com/en-au/magazine/cc163715.aspx
p. s. что вызвало этот очень поздний ответ? Все ответы были настолько вопиюще неправильными (особенно тот, который отмечен как ответ) в их объяснении, я просто должен был прояснить это для всех, кто это читает. пожимает
p.p. s.Я предполагаю, что целью является x86/x64, а не IA64 (у него другая модель памяти). Обратите внимание, что спецификации ECMA от Microsoft испорчены что он указывает самую слабую модель памяти вместо самой сильной (всегда лучше указывать против самой сильной модели памяти, чтобы она была согласована на разных платформах - в противном случае код, который будет работать 24-7 на x86 / x64, может вообще не работать на IA64, хотя Intel реализовала аналогичную сильную модель памяти для IA64) - Microsoft признала это сама -http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx.
Блокированные функции не блокируются. Они являются атомарными, что означает, что они могут завершаться без возможности переключения контекста во время инкремента. Так что нет никаких шансов на тупик или ждать.
Я бы сказал, что вы всегда должны предпочесть его блокировке и приращению.
Volatile полезен, если вам нужно, чтобы записи в одном потоке считывались в другом, и если вы хотите, чтобы оптимизатор не переупорядочивал операции над переменной (потому что все происходит в другом потоке что оптимизатор не знает). Это ортогональный выбор того, как вы увеличиваете.
Это действительно хорошая статья, если вы хотите узнать больше о lock-free code, и правильный подход к его написанию
замок(...) работает, но может блокировать поток и может вызвать взаимоблокировку, если другой код использует те же блокировки несовместимым образом.
блокировать.* правильный способ сделать это ... гораздо меньше накладных расходов, поскольку современные процессоры поддерживают это как примитив.
volatile сам по себе не является правильным. Поток, пытающийся получить и затем записать измененное значение, все еще может конфликтовать с другим потоком, делающим то же самое.
Я второй ответ Джона Скита и хочу добавить следующие ссылки для всех, кто хочет узнать больше о "volatile" и Interlocked:
атомарность, летучесть и неизменность различны, часть вторая
атомарность, летучесть и неизменность различны, часть три
Sayonara Volatile - (Wayback Machine снимок блога Джо Даффи, как он появился в 2012 году)
Я сделал некоторые тесты, чтобы увидеть, как теория на самом деле работает:kennethxu.blogspot.com/2009/05/interlocked-vs-monitor-performance.html. мой тест был более сосредоточен на CompareExchnage, но результат для инкремента аналогичен. Блокировка не требуется быстрее в среде с несколькими процессорами. Вот результат теста для инкремента на 2-летнем 16-процессорном сервере. Голые в виду, что тест также включает в себя безопасное чтение после увеличения, что типично в реальном мире.
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
читать многопоточность в C# ссылка. Он охватывает все входы и выходы вашего вопроса. Каждый из трех имеют различные цели и побочные эффекты.