Летучий против блокировки и блокировки

604

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

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

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

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

  • 0
    Прочитайте ссылку на Threading в C # . Он охватывает все тонкости вашего вопроса. Каждый из трех имеет разные цели и побочные эффекты.
  • 1
    simple-talk.com/blogs/2012/01/24/… вы можете увидеть использование volitable в массивах, я не совсем понимаю, но это еще одна ссылка на то, что это делает.
Показать ещё 3 комментария
Теги:
multithreading
locking
interlocked
volatile

10 ответов

735
Лучший ответ

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

Измените модификатор доступа counter на public volatile

Как говорили другие люди, это само по себе вообще не безопасно. Точка volatile заключается в том, что несколько потоков, работающих на нескольких процессорах, могут и будут кэшировать данные и переназначать инструкции.

Если он не является volatile, а CPU A увеличивает значение, тогда CPU B может фактически не видеть это увеличиваемое значение до некоторого времени позже, что может вызвать проблемы.

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

Второе место:

lock(this.locker) this.counter++;

Это безопасно делать (при условии, что вы помните, чтобы lock везде, где вы this.counter к this.counter). Он предотвращает выполнение другими потоками любого другого кода, который защищен locker. Использование блокировок также предотвращает проблемы с переупорядочением нескольких процессоров, как указано выше, что отлично.

Проблема в том, что блокировка медленная, и если вы повторно используете locker в каком-то другом месте, которое действительно не связано, вы можете в конечном итоге заблокировать ваши другие потоки без каких-либо причин.

Лучший

Interlocked.Increment(ref this.counter);

Это безопасно, поскольку он эффективно выполняет чтение, увеличение и запись в "одном ударе", который не может быть прерван. Из-за этого он не будет влиять на какой-либо другой код, и вам не нужно забывать о блокировке в другом месте. Это также очень быстро (как говорит MSDN, на современных процессорах это часто буквально одна команда CPU).

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

InterlockedNotes:

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

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

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

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

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

  • 27
    «Я не совсем уверен ... нужно ли вам комбинировать энергозависимость с приращением». Они не могут быть объединены AFAIK, так как мы не можем передать изменчивый реф. Отличный ответ, кстати.
  • 37
    Большое спасибо! Ваша сноска на тему «Что на самом деле хорошо для volatile» - это то, что я искал и подтвердил, как я хочу использовать volatile.
Показать ещё 31 комментарий
135

EDIT: Как отмечалось в комментариях, в эти дни я с удовольствием использую Interlocked для случаев с одной переменной, где это очевидно. Когда это усложняется, я все равно вернусь к блокировке...

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

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

  • 14
    +1 за то, что теперь я могу забыть о кодировании без блокировки.
  • 5
    Коды без блокировок определенно не являются по-настоящему свободными от блокировок, поскольку они блокируются на каком-то этапе - будь то на уровне шины (FSB) или на уровне межплатного процессора, вам все равно придется заплатить штраф. Однако блокировка на этих более низких уровнях обычно выполняется быстрее, если вы не насыщаете полосу пропускания того места, где происходит блокировка.
Показать ещё 10 комментариев
41

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

Приращение переменной требует фактически трех операций:

  • прочитать
  • приращение
  • записи

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

  • 3
    Надеюсь, вы не имеете в виду cached как в кэшировании в кэше процессора !!!
  • 4
    Иными словами, заблокированные изменения полностью защищены и поэтому являются атомарными. Летучие члены имеют только частичное ограждение и поэтому не гарантируют поточно-ориентированную защиту.
Показать ещё 1 комментарий
38

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

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

например.

while (m_Var)
{ }

Если m_Var установлен в false в другом потоке, но он не объявлен как volatile, компилятор может сделать его бесконечным циклом (но это не значит, что он всегда будет), заставив его проверить регистр CPU (например, 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 (у нее другая модель памяти). Обратите внимание, что спецификации Microsoft ECMA ограничены тем, что он определяет самую слабую модель памяти, а не самую сильную (всегда лучше указывать против самой сильной модели памяти, поэтому она совместима между платформами - иначе код, который будет работать 24-7 на x86/x64 может вообще не работать на IA64, хотя Intel реализовала аналогичную сильную модель памяти для IA64) - Microsoft признала это сама - http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx.

  • 3
    Интересно. Можете ли вы сослаться на это? Я бы с радостью проголосовал за это, но публикация на каком-то агрессивном языке через 3 года после высоко оцененного ответа, соответствующего прочитанным мною ресурсам, потребует чуть более ощутимых доказательств.
  • 0
    Если вы можете указать, на какую часть вы хотите сослаться, я был бы рад откопать кое-что откуда-то (я очень сомневаюсь, что отдал какие-либо торговые секреты поставщика x86 / x64, так что они должны быть легко доступны из вики, Intel PRMs (справочник программиста), блоги MSFT, MSDN или что-то подобное) ...
Показать ещё 10 комментариев
15

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

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

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

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

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

11

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

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

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

7

Я сделал несколько тестов, чтобы увидеть, как работает теория: kenneспасибоu.blogspot.com/2009/05/interlocked-vs-monitor-performance.html. Мой тест был более сосредоточен на CompareExchnage, но результат для Increment аналогичен. В среде с несколькими процессорами блокировка не требуется быстрее. Вот результат теста для Increment на 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
  • 0
    Пример кода, который вы тестировали, был настолько тривиален, хотя на самом деле нет смысла тестировать его таким образом! Лучше всего было бы понять, что на самом деле делают различные методы, и использовать соответствующий, основываясь на сценарии использования, который у вас есть.
  • 0
    @ Зач, как здесь обсуждался сценарий увеличения счетчика потокобезопасным способом. Какой еще сценарий использования был у вас на уме или как бы вы его протестировали? Спасибо за комментарий Кстати.
Показать ещё 2 комментария
4

Прочтите ссылку Threading in С#. Он охватывает все аспекты вашего вопроса. Каждый из трех имеет разные цели и побочные эффекты.

1

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

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

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню