Летучий против блокировки и блокировки
Скажем, у класса есть public int counter
поле, доступ к которому осуществляется несколькими потоками. это int
только увеличивается или уменьшается.
Чтобы увеличить это поле, какой подход следует использовать и почему?
lock(this.locker) this.counter++;
,Interlocked.Increment(ref this.counter);
,- Изменить модификатор доступа
counter
вpublic volatile
,
Теперь, когда я обнаружил volatile
Я удаляю много lock
заявления и использование Interlocked
, Но есть ли причина не делать этого?
11 ответов
Худший (на самом деле не будет работать)
Изменить модификатор доступа
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, на современных ЦП это часто буквально одна инструкция ЦП).
Я не совсем уверен, однако, если это обойти другие процессоры, переупорядочивающие вещи, или если вам также нужно объединить volatile с приращением.
InterlockedNotes:
- Взаимосвязанные методы являются одновременно безопасными для любого количества ядер или процессоров.
- Блокированные методы применяют полный барьер вокруг выполняемых команд, поэтому переупорядочение не происходит.
- Методы с блокировкой не нуждаются или даже не поддерживают доступ к изменчивому полю, поскольку volatile размещает половину ограждения вокруг операций на заданном поле, а блокированное использует полное ограждение.
Сноска: Что на самом деле хорошо для летучих?
Как volatile
не предотвращает такого рода проблемы многопоточности, для чего это нужно? Хороший пример говорит, что у вас есть два потока, один из которых всегда записывает в переменную (скажем, queueLength
) и тот, который всегда читает из этой же переменной.
Если queueLength
не является энергозависимым, поток A может записывать пять раз, но поток B может увидеть эти записи как задержанные (или даже потенциально в неправильном порядке).
Решением будет блокировка, но вы также можете использовать volatile в этой ситуации. Это гарантировало бы, что поток B всегда будет видеть самую последнюю вещь, которую написал поток A. Однако обратите внимание, что эта логика работает, только если у вас есть писатели, которые никогда не читают, и читатели, которые никогда не пишут, и если то, что вы пишете, является атомарной ценностью. Как только вы выполните одну операцию чтения-изменения-записи, вам нужно перейти к блокированным операциям или использовать блокировку.
РЕДАКТИРОВАТЬ: Как отмечено в комментариях, в эти дни я счастлив использовать Interlocked
для случаев одной переменной, где это, очевидно, хорошо. Когда все станет сложнее, я все равно вернусь к блокировке...
С помощью volatile
не поможет, когда вам нужно увеличить - потому что чтение и запись являются отдельными инструкциями. Другой поток может изменить значение после того, как вы прочитали, но перед тем, как написать обратно.
Лично я почти всегда просто блокируюсь - легче сделать правильный путь, который, очевидно, прав, чем волатильность или взаимосвязь. Инкремент. Насколько я понимаю, многопоточность без блокировок предназначена для настоящих экспертов по многопоточности, из которых я не один. Если Джо Даффи и его команда создадут хорошие библиотеки, которые будут распараллеливать вещи без такой большой блокировки, как то, что я бы построил, это невероятно, и я буду использовать это в одно мгновение - но когда я сам делаю потоки, я пытаюсь будь проще.
"volatile
"не заменяет Interlocked.Increment
! Это просто гарантирует, что переменная не кэшируется, а используется напрямую.
Увеличение переменной требует фактически трех операций:
- читать
- приращение
- записывать
Interlocked.Increment
выполняет все три части как одну атомарную операцию.
Либо блокировка, либо инкрементное приращение - это то, что вы ищете.
Volatile определенно не то, что вам нужно - она просто указывает компилятору обрабатывать переменную как всегда изменяющуюся, даже если текущий путь кода позволяет компилятору оптимизировать чтение из памяти в противном случае.
например
while (m_Var)
{ }
если m_Var установлен в false в другом потоке, но он не объявлен как volatile, компилятор может сделать его бесконечным циклом (но это не значит, что так будет всегда), проверяя регистр процессора (например, EAX, потому что это было то, что m_Var был извлечен с самого начала) вместо того, чтобы выдать другое чтение в ячейку памяти m_Var (это может быть кэшировано - мы не знаем и не заботимся, и в этом суть когерентности кэша x86/x64). Все ранее опубликованные сообщения, в которых упоминалось переупорядочение инструкций, просто показывают, что они не понимают архитектуры x86 / x64. Volatile не создает барьеров для чтения / записи, как это подразумевалось в предыдущих публикациях, в которых говорится, что "это предотвращает изменение порядка". Фактически, еще раз благодаря протоколу 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 полезно, если вам нужно, чтобы записи в одном потоке читались в другом, и если вы хотите, чтобы оптимизатор не переупорядочивал операции с переменной (потому что события происходят в другом потоке, о котором оптимизатор не знает). Это ортогональный выбор того, как вы увеличиваете.
Это действительно хорошая статья, если вы хотите узнать больше о коде без блокировки и о том, как правильно его написать.
Блокировка (...) работает, но может блокировать поток и может вызвать взаимоблокировку, если другой код использует такие же блокировки несовместимым способом.
Interlocked. * - это правильный способ сделать это... намного меньше накладных расходов, поскольку современные процессоры поддерживают это как примитив.
изменчиво само по себе не правильно. Поток, пытающийся получить и затем записать измененное значение, все еще может конфликтовать с другим потоком, делающим то же самое.
Я провел некоторый тест, чтобы увидеть, как на самом деле работает теория: http://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
Я второй ответ Джона Скита и хочу добавить следующие ссылки для всех, кто хочет узнать больше о "volatile" и Interlocked:
Атомность, изменчивость и неизменность различны, часть вторая
Атомность, изменчивость и неизменность различны, часть третья
Sayonara Volatile - (снимок Wayback Machine из блога Джо Даффи, как он появился в 2012 году)
Прочитайте ссылку на Threading в C#. Он охватывает все тонкости вашего вопроса. Каждый из трех имеет разные цели и побочные эффекты.
Я хотел бы добавить к упомянутым в других ответах разницу между volatile, Interlocked и lock:
Ключевое слово volatile может применяться к полям следующих типов:
- Типы ссылок.
- Типы указателей (в небезопасном контексте). Обратите внимание, что, хотя сам указатель может быть изменчивым, объект, на который он указывает, не может. Другими словами, вы не можете объявить "указатель на volatile".
- Простые типы, такие как sbyte, byte, short, ushort, int, uint, char, float и bool.
- Тип enum с одним из следующих базовых типов: byte, sbyte, short, ushort, int или uint.
- Параметры общего типа, как известно, являются ссылочными типами.
- IntPtr и UIntPtr.
Другие типы, включая double и long, не могут быть помечены как volatile, поскольку чтение и запись в поля этих типов не могут быть гарантированно атомарными. Чтобы защитить многопоточный доступ к этим типам полей, используйте члены класса Interlocked или защитите доступ с помощью оператора блокировки.
Я здесь только для того, чтобы указать на ошибку относительно volatile в ответе Ориона Эдвардса.
Он сказал:
«Если он изменчив, это просто гарантирует, что два процессора видят одни и те же данные одновременно».
Это не правильно. В документе Microsoft о volatile упоминается:
«В многопроцессорной системе операция чтения в энергозависимой памяти не гарантирует получения последнего значения, записанного в эту ячейку памяти любым процессором. Точно так же операция энергозависимой записи не гарантирует, что записанное значение будет немедленно видно другим процессорам».