Вопрос

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

Какой подход следует использовать для увеличения этого поля и почему?

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

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

Это было полезно?

Решение

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

Измените модификатор доступа 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, на современных процессорах это часто буквально одна инструкция процессора).

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

ВзаимосвязанныеПримечания:

  1. СВЯЗАННЫЕ МЕТОДЫ ОДНОВРЕМЕННО БЕЗОПАСНЫ НА ЛЮБОМ КОЛИЧЕСТВЕ ЯДЕР ИЛИ ЦП.
  2. Взаимосвязанные методы применяют полный барьер вокруг выполняемых ими инструкций, поэтому переупорядочение не происходит.
  3. Взаимосвязанные методы не нужен или даже не поддерживает доступ к изменчивому полю, поскольку волатильный объект размещается полузабором вокруг операций в данном поле, а блокировка использует полный забор.

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

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

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

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

Другие советы

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

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

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

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

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

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

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

Либо блокировка, либо взаимосвязанное приращение — это то, что вам нужно.

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

например

while (m_Var)
{ }

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

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

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

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

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

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

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

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

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

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

Я провел небольшой тест, чтобы увидеть, как на самом деле работает теория: kennethxu.blogspot.com/2009/05/interlocked-vs-monitor- Performance.html.Мой тест был больше ориентирован на CompareExchnage, но результат для Increment аналогичен.Блокировка не требуется быстрее в многопроцессорной среде.Вот результат теста Increment на 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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top