Летучий против Блокированный против блокировки - PullRequest
628 голосов
/ 30 сентября 2008

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

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

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

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

Ответы [ 9 ]

810 голосов
/ 01 октября 2008

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

Изменить модификатор доступа 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. Однако обратите внимание, что эта логика только работает, если у вас есть писатели, которые никогда не читают, и читатели, которые никогда не пишут, и , если то, что вы пишете, имеет атомарную ценность. Как только вы выполните одну операцию чтения-изменения-записи, вам нужно перейти к блокированным операциям или использовать блокировку.

135 голосов
/ 30 сентября 2008

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

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

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

42 голосов
/ 30 сентября 2008

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

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

  1. прочитать
  2. прибавка
  3. запись

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

41 голосов
/ 23 июня 2011

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

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

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

p.p.s. Я предполагаю, что целью является 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.

15 голосов
/ 30 сентября 2008

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

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

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

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

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

11 голосов
/ 30 сентября 2008

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

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

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

7 голосов
/ 25 мая 2009

Я провел некоторый тест, чтобы увидеть, как на самом деле работает теория: 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 голосов
/ 02 декабря 2018

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

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

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

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

...