Неустойчивый против блокировки против блокировки

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

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

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

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

simple-talk.com/blogs/2012/01/24/… you can see the use of volitable in arrays , i don't completly understand it , but it's anouther reference to what this does.
eran otzap 28.09.2013 20:18

Это все равно что сказать: «Я обнаружил, что спринклерная система никогда не активируется, поэтому я собираюсь удалить ее и заменить на детекторы дыма». Причина не делать этого - потому что это невероятно опасно и почти не приносит вам пользы. Если есть время потратить на смену кода, то найти способ сделать его менее многопоточным! Не ищите способ сделать многопоточный код более опасным и легко взломанным!

Eric Lippert 18.02.2014 21:25

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

yoyo 05.11.2014 03:17

@yoyo Нет, вам не нужны оба.

David Schwartz 15.10.2016 00:17

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

spoulson 30.09.2008 23:33
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
713
5
144 393
9
Перейти к ответу Данный вопрос помечен как решенный

Ответы 9

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

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

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

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

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

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

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

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

+1 за то, что заставил меня забыть о кодировании без блокировок.

Xaqron 03.01.2011 04:51

Коды без блокировки определенно не являются действительно свободными от блокировки, поскольку они блокируются на определенном этапе - будь то на уровне шины (FSB) или на уровне между ЦП, вам все равно придется заплатить штраф. Однако блокировка на этих более низких уровнях обычно выполняется быстрее, если вы не насыщаете полосу пропускания там, где происходит блокировка.

Zach Saw 07.07.2011 05:25

Нет ничего плохого в Interlocked, это именно то, что вы ищете и быстрее, чем полная блокировка ()

Jaap 23.03.2012 00:24

@Jaap: Да, в наши дни я использую блокировку бы для подлинного одиночного счетчика. Я просто не хотел бы начинать возиться, пытаясь выработать взаимодействие между обновлениями переменных без блокировки несколько.

Jon Skeet 23.03.2012 00:30

@ZachSaw: Быть «свободным от блокировки» не означает, что часть кода всегда будет работать с одной и той же скоростью независимо от разногласий. Скорее, это означает, что существует ограничение на то, как долго поток может находиться в ожидании вследствие того, что другой поток задерживается на неопределенное время (в коде на основе блокировки, если поток задерживается, удерживая блокировку, потоки, которым требуется блокировка, могут блокироваться на неопределенный срок). Обратите внимание, что простое отсутствие блокировок не гарантирует, что коду не придется бесконечно ждать ресурс, о котором идет много споров, но функции Interlocked также предоставляют последнюю гарантию.

supercat 14.08.2012 19:04

@supercat: А? Кому или чему вы отвечаете ??

Zach Saw 17.08.2012 11:18

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

supercat 17.08.2012 18:55

@supercat: Какой комментарий, по моему мнению, при блокировке операций может привести к блокировке автобуса на неопределенный срок?

Zach Saw 21.08.2012 10:22

@ZachSaw: Ваш второй комментарий говорит, что заблокированные операции на каком-то этапе "блокируются"; термин «блокировка» обычно подразумевает, что одна задача может поддерживать монопольный контроль над ресурсом в течение неограниченного периода времени; Основное преимущество программирования без блокировок состоит в том, что оно позволяет избежать опасности того, что ресурс станет непригодным для использования в результате задержания задачи-владельца. Синхронизация шины, используемая блокируемым классом, не просто «в целом более быстрая» - в большинстве систем она имеет ограниченное время наихудшего случая, тогда как блокировки этого не делают.

supercat 21.08.2012 19:01

@supercat: Если вы так интерпретируете слово «блокировка», то «заблокированный», которое включает слово «блокировка», будет означать то же самое - это явно не так!

Zach Saw 28.08.2012 07:06

Если вы скажете: «Многопоточность без блокировок предназначена для настоящих экспертов по многопоточности, к которым я не отношусь». Я никогда не буду пытаться писать многопоточный код без блокировок. :)

Teoman shipahi 29.09.2016 18:18

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

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

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

«volatile» не заменяет Interlocked.Increment! Он просто следит за тем, чтобы переменная не кешировалась, а использовалась напрямую.

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

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

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

Другими словами, заблокированные изменения полностью изолированы и как таковые являются атомарными. Неустойчивые члены ограничены лишь частично и поэтому не гарантируют многопоточность.

JoeGeeky 04.12.2011 23:58

На самом деле volatile делает нет, что переменная не кэширована. Он просто накладывает ограничения на то, как его можно кэшировать. Например, его все еще можно кэшировать в кеш-памяти второго уровня ЦП, потому что они согласованы на оборудовании. Его все еще можно префектить. Записи по-прежнему могут быть отправлены в кеш и т. д. (Думаю, именно это и имел в виду Зак.)

David Schwartz 04.12.2015 15:36
Ответ принят как подходящий

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

Change the access modifier of counter to public volatile

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

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

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

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

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

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

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

Лучший

Interlocked.Increment(ref this.counter);

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

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

Примечания:

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

Сноска: для чего на самом деле годится volatile.

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

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

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

«Я не совсем уверен ... нужно ли вам также комбинировать изменчивость с инкрементом». Они не могут быть объединены AFAIK, так как мы не можем передать volatile по ссылке. Кстати, отличный ответ.

Hosam Aly 17.01.2009 16:07

Большое спасибо! Ваша сноска «Чем на самом деле годится volatile?» - это то, что я искал, и подтвердил, что хочу использовать volatile.

Jacques Bosch 10.05.2010 10:22

+1: В CLR через C# есть раздел о volatile, который сводится именно к этому. Термин для ситуации, которую решает энергозависимый, называется когерентностью кэша (en.wikipedia.org/wiki/Cache_coherence).

Steven Evers 02.06.2010 02:29

Другими словами, если переменная объявлена ​​как изменчивая, компилятор будет предполагать, что значение переменной не будет оставаться прежним (т. Е. Изменчивым) каждый раз, когда ваш код встречает ее. Таким образом, в таком цикле, как while (m_Var) {} и m_Var установлено значение false в другом потоке, компилятор не будет просто проверять, что уже находится в регистре, который ранее был загружен значением m_Var, а считывает значение из m_Var очередной раз. Однако это не означает, что отсутствие объявления volatile приведет к тому, что цикл будет продолжаться бесконечно - указание volatile только гарантирует, что этого не произойдет, если для m_Var установлено значение false в другом потоке.

Zach Saw 23.06.2011 11:41

@SnOrfus: Неправильно. Volatile не имеет ничего общего с когерентностью кеша. Согласованность поддерживается независимо от того, что вы делаете, и встроена в центральные процессоры. См. Мой ответ выше.

Zach Saw 07.07.2011 05:26

@Zach Saw: Согласно модели памяти для C++, volatile - это то, как вы это описали (в основном полезно для памяти, отображаемой на устройство, и не для чего-то еще). Согласно модели памяти для CLR (этот вопрос помечен как C#), volatile будет вставлять барьеры памяти вокруг чтения и записи в это место хранения. Барьеры памяти (и специальные заблокированные варианты некоторых инструкций по сборке) - вы говорите процессор не менять порядок вещей, и они довольно важны ...

Orion Edwards 08.07.2011 07:39

@Zach Saw: Хотя вы правы, что в наши дни в большинстве систем с несколькими процессорами согласованность кеша будет поддерживать данные каждого процессора в актуальном состоянии, не будет учитывает тот факт, что процессор может (и выполняет) переупорядочивать инструкции . Вам нужны барьеры памяти, чтобы предотвратить это, и, как я уже упоминал выше, энергозависимость в CLR создает барьеры памяти вокруг каждого чтения / записи в энергозависимое хранилище ...

Orion Edwards 08.07.2011 07:49

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

Zach Saw 08.07.2011 08:44

@ Орион: Ты ошибаешься. Прочтите размещенные мною ссылки. Учитывая сильную модель памяти x86 / x64, volatile просто заставляет компилятор определенным образом переупорядочивать инструкции нет. Остальное по-прежнему полагается на модель памяти x86 / x64.

Zach Saw 08.07.2011 08:45

@Orion: Да, модель памяти CLR отличается от C++ - я никоим образом не описывал модель памяти C++. Я описывал модель памяти x86 / x64. CLR работает поверх него, и когда уже гарантирована сильная модель памяти, зачем CLR делать что-то еще, чтобы замедлить работу ???

Zach Saw 08.07.2011 08:47

@Orion: Вы должны понимать, что инструкции по повторному заказу нет влияют на результат сохранения (и, следовательно, на чтение из другого процессора), если сохранение выполняется по порядку. Барьера памяти после a store достаточно для всех целей и задач. Это гарантирует, что сигнализация ожидающего потока всегда будет видеть зафиксированное значение. В C++ нет гарантирует это с помощью ключевого слова volatile - вы должны сделать барьер самостоятельно. Базовая модель памяти, гарантированная CLR, - это слабая модель памяти - в CLR нет необходимость для создания барьеров памяти - это зависит от поставщика.

Zach Saw 08.07.2011 08:56

прагматично волатильный, по крайней мере, исправит его проблему.

Tom Fobear 02.12.2011 00:33

@ZachSaw, извини, но ты немного ошибаешься. Ознакомьтесь с этой статьей эксперта: blogs.msdn.com/b/ericlippert/archive/2011/06/16/… - В C# «изменчивый» означает не только «убедитесь, что компилятор и джиттер не производят переупорядочение кода или не регистрируют оптимизацию кэширования для этой переменной». Это также означает «приказать процессорам сделать все, что им нужно, чтобы убедиться, что я читаю последнее значение, даже если это означает остановку других процессоров и их синхронизацию с основной памятью со своими кэшами».

zihotki 30.04.2012 01:56

@zihotki: Это простая версия. Посмотрите комментарии - автору задавали конкретные вопросы о согласованности кеша, и его ответ был тем, что я пытался объяснить, очевидно, безрезультатно для некоторых ...

Zach Saw 03.05.2012 14:55

@ZachSaw вздохнул ... Я не сомневаюсь, что вы знаете больше, чем я, о согласованности и переупорядочении, но вы, похоже, упускаете тот момент, что вопрос - это вопрос C#, поэтому мы говорим о модели памяти CLR, а НЕ о x86 / 64 или любая другая модель памяти

Orion Edwards 09.05.2012 03:39

@Orion: И мы вернулись на круги своя. Вы сказали, что CLR создает барьеры памяти вокруг операций, включающих изменчивые переменные. Я сказал, что не обязательно (есть и другие способы реализации, как показано в модели памяти x86). Теперь это яснее?

Zach Saw 10.05.2012 08:42

@ZachSaw удивительно, да :-) Ура

Orion Edwards 17.05.2012 01:01

@ZachSaw: изменчивое поле в C# не позволяет компилятору C# и компилятору jit выполнять определенные оптимизации, которые могут кэшировать значение. Он также дает определенные гарантии относительно того, какой порядок чтения и записи может наблюдаться в нескольких потоках. В качестве детали реализации это может быть сделано путем введения барьеров памяти при чтении и записи. Гарантированная точная семантика описана в спецификации; обратите внимание, что в спецификации нет гарантирует, что последовательный упорядочение энергозависимых операций записи и чтения все будет соблюдаться потоками все.

Eric Lippert 03.12.2013 21:22

@ZachSaw - просто перечитал комментарии давней давности, и я понял, что этот мой комментарий был неправильным - «Согласно модели памяти для CLR, энергозависимая память будет вставлять барьеры памяти вокруг операций чтения и записи в это место хранения». - CLR вставит барьеры памяти для некоторых изменчивых операций на ARM из-за более слабой модели памяти - но вы правы, он не делает этого для x86, потому что в этом нет необходимости. Мои извинения

Orion Edwards 08.01.2014 23:26

@OrionEdwards Прочитав все комментарии, я не уверен, верен ли ваш принятый ответ. Не могли бы вы помочь и дополнить свой ответ?

Mickey 29.01.2014 11:53

@OrionEdwards Это действительно модель CLR (реализация интерфейса командной строки MSFT). Однако интерфейс командной строки ECMA определяет слабую модель памяти, поэтому ее реализация зависит от поставщика. Но для практических целей оставим это как заключение. С академической точки зрения, спецификации ECMA CLI - это крушение.

Zach Saw 14.02.2014 06:52

@EricLippert Какая это спецификация? CLR или CLI?

Zach Saw 14.02.2014 06:53

@ZachSaw: спецификация C#.

Eric Lippert 14.02.2014 19:39

@EricLippert C# ECMA-334 указывает, что изменчивые поля обеспечивают семантику получения-выпуска. Помните, я не утверждаю, что это не нужно для более слабых моделей памяти. Сказать, что изменчивость означает, что JIT будет вводить инструкции барьера памяти, явно неверно. Как неоднократно объяснялось, сильная модель памяти x86 дает тот же эффект без необходимости явно указывать барьеры памяти. Таким образом, volatile ведет себя очень похоже на C++ (не совсем то же самое).

Zach Saw 18.02.2014 09:38

@ZachSaw: Да: в спецификации сказано, что семантика получения и выпуска будет зависеть от изменчивых операций чтения и записи. Как среда выполнения решит это сделать, зависит от нее; если нет может обойтись без полного или половинного ограничения инструкция из-за какой-либо другой гарантии, предоставляемой конкретным процессором, то он не обязан генерировать ненужный код.

Eric Lippert 18.02.2014 21:20

@EricLippert Да. Это именно то, что я пытался объяснить. Рад видеть, что всех наконец догнали!

Zach Saw 19.02.2014 03:47

Мне лично очень нравится объяснение модели памяти Игоря Островского. Заставляет вас понять, что на самом деле делает volatile. msdn.microsoft.com/en-us/magazine/jj883956.aspx Еще у него есть отличный блог igoro.com.

FrankyHollywood 24.03.2014 15:35

Не будет "блокировать (this.locker) ++ this.counter;" избежать считывания до того, как оптимизатор доберется до него?

John Taylor 02.02.2015 22:08

@JohnTaylor Я не уверен, что понимаю, о чем вы спрашиваете? ЦП всегда должен считывать значение из памяти, чтобы увеличить его и записать обратно, независимо от того, как оно увеличивается.

Orion Edwards 04.02.2015 00:51

С ++ this.counter компилятор сгенерирует что-то вроде inc [ebx + 0x12] с this.counter ++, он сделает это с "mov eax, [ebx + 0x12]; inc eax; mov [ebx + 0x12], eax" Конечно, оптимизатор уже может сделать то же самое.

John Taylor 04.02.2015 01:16

@JohnTaylor, да. Я проверил сборку, созданную CLR JIT, и она испускала единственную инструкцию для постинкремента, так что оптимизатор, очевидно, до нее добрался. В любом случае, inc [ebx + 0x12] по-прежнему включает в себя ЦП, выполняющий чтение из памяти, приращение и запись в память.

Orion Edwards 05.02.2015 03:24

Обратите внимание, что при использовании Interlocked.Increment для генерации уникальных значений жизненно важно использовать возвращаемое значение. Если вы просто хотите гарантировать, что никакие приращения не будут «потеряны» из-за плохого чередования потоков, это не имеет значения.

Brian 14.02.2019 19:03

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

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

Zach Saw 23.06.2011 19:10

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

Kenneth Xu 28.06.2011 01:05

Дело в том, что это искусственный тест. Вы не собираетесь забивать одну и ту же локацию, как это часто бывает в любом реальном мире. Если да, то, что ж, у вас узкое место в ФСБ (как показано в полях вашего сервера). Во всяком случае, посмотрите мой ответ в своем блоге.

Zach Saw 07.07.2011 05:18

Снова оглядываясь назад. Если истинное узкое место связано с системной шиной, реализация монитора должна иметь такое же узкое место. Реальная разница в том, что Interlocked выполняет ожидание и повтор при занятости, что становится реальной проблемой при подсчете высокой производительности. По крайней мере, я надеюсь, что мой комментарий привлечет внимание к тому, что Interlocked не всегда является правильным выбором для подсчета. Тот факт, что люди ищут альтернативы, хорошо объяснил это. Нужен длинный сумматор gee.cs.oswego.edu/dl/jsr166/dist/jsr166edocs/jsr166e/…

Kenneth Xu 06.10.2013 20:01

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

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

например

while (m_Var)
{ }

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

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

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

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

Для получения дополнительной информации см. «Понимание влияния методов Low-Lock в многопоточных приложениях» - 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 года после высоко оцененного ответа, который согласуется с ресурсами, которые я прочитал, потребует немного более ощутимых доказательств.

Steven Evers 07.07.2011 07:28

Если вы можете указать, на какую часть вы хотите сослаться, я был бы рад выкопать кое-что откуда-то (я очень сомневаюсь, что я раскрыл какие-либо коммерческие секреты поставщиков x86 / x64, поэтому они должны быть легко доступны через вики, Intel PRM (справочные руководства для программистов), блоги MSFT, MSDN или что-то подобное) ...

Zach Saw 07.07.2011 08:19

Кроме того, я не думаю, что это так несовместимо с тем, что ответили другие, только в их объяснении - предполагая, что volatile не позволяет ЦП кэшировать результат переменной. Это просто смешно. Как это согласуется со всем, что вы читали? Если вы можете найти что-либо в x86 / x64, что позволяет вам установить только 32-разрядную / 64-разрядную область памяти для сквозного кеширования или Windows, позволяющую клиентам изменять конкретную область памяти для сквозной записи на лету, и затем отрегулируйте это соответственно, когда GC сжимает кучу, следовательно, минуя кеш процессора ...

Zach Saw 07.07.2011 08:27

Я не понимаю, зачем кому-то мешать кэширование ЦП. Вся недвижимость (определенно немаловажная по размеру и стоимости), предназначенная для обеспечения согласованности кеша, будет полностью потрачена впустую, если это так ... Если вам не требуется согласованность кеша, такая как видеокарта, устройство PCI и т. д., Вы бы не установили строка кэша для сквозной записи.

Zach Saw 07.07.2011 08:29

Да, все, что вы говорите, если не на 100%, то хотя бы на 99% точно. Этот сайт (в основном) очень полезен, когда вы находитесь в спешке с разработкой на работе, но, к сожалению, точность ответов, соответствующих (игре) голосов, отсутствует. Таким образом, в основном в stackoverflow вы можете получить представление о том, что популярно среди читателей, а не о том, что есть на самом деле. Иногда главные ответы - это просто тарабарщина, своего рода мифы. И, к сожалению, именно это и впадает в людей, которые сталкиваются с прочитанным при решении проблемы. Но это понятно, никто не может знать всего.

user1416420 14.12.2012 12:03

Проблема с этим ответом и вашими комментариями по этому вопросу заключается в том, что он предназначен исключительно для x86, тогда как вопроса не было. Информация о базовой модели аппаратной памяти иногда бывает полезна, но не заменяет знания о модели памяти CLR. Например, тот факт, что барьер памяти неявен на x86, не означает, что модель памяти CLR не требует барьеров памяти для volatile (больше, чем C++ volatile). Код .NET работает на полдюжине архитектур, а C++ - гораздо больше.

Ben Voigt 10.02.2013 20:19

@BenVoigt Я мог бы продолжить и ответить обо всех архитектурах, на которых работает .NET, но это займет несколько страниц и определенно не подходит для SO. Гораздо лучше обучать людей на основе наиболее широко используемой базовой аппаратной мем-модели .NET, чем на произвольной. И своими комментариями «везде» я исправлял ошибки, которые люди допускали, предполагая очистку / аннулирование кеша и т. д. Они делали предположения о базовом оборудовании, не указывая, какое оборудование.

Zach Saw 11.02.2013 02:39

@BenVoigt Также обратите внимание, что Microsoft для начала наполнила свои спецификации модели памяти ECMA, поэтому на такой вопрос нет однозначного ответа. В вопросе также не упоминается модель памяти CLR. Стандарт ECMA, а не CLR. И ECMA не требует ограничения памяти. Так что, возможно, вы просто проявили педантичность, когда разместили свой комментарий?

Zach Saw 11.02.2013 02:45

@Zach: Даже модель ECMA дает семантику получения и / или выпуска (не полный барьер, но это также типы барьеров / ограждений) для изменчивого доступа, не так ли?

Ben Voigt 11.02.2013 05:49

@BenVoigt: Из памяти он указывает слабую модель памяти - следовательно, нет никаких гарантий для изменчивого доступа. CLR, с другой стороны, реализует сильную модель памяти на IA64.

Zach Saw 11.02.2013 10:17

@Zach: Согласно Объяснение Microsoft, на которое вы ссылаетесь, он не такой уж и слабый. Он не делает ничего лишнего на x86, потому что x86 сам дает более строгие гарантии, но не путайте это с мыслью, что volatile не несет никаких гарантий.

Ben Voigt 11.02.2013 10:57

@BenVoigt: Как я уже сказал в своем ответе, я не делаю никаких предположений относительно каких-либо других архитектур (другие были - что побудило меня опубликовать этот ответ). Однако без ссылки (из официального источника Microsoft на ECMA std) это почти догадки с обеих сторон.

Zach Saw 12.02.2013 04:28

@ZachSaw, единственный хороший ответ здесь и самый первый, который я прочитал, который действительно четко объясняет, почему и для чего используется "Volatile". Большое вам спасибо.

Eric Ouellet 09.12.2020 22:09

@ZachSaw • 8.10 Порядок выполнения Порядок побочных эффектов сохраняется в отношении изменчивых операций чтения и записи. • 15.5.4 Неустойчивые поля ◾ Чтение изменчивого поля называется изменчивым чтением. Неустойчивое чтение имеет «семантику приобретения»; то есть гарантируется, что это произойдет до любых обращений к памяти, которые происходят после него в последовательности команд.Запись изменчивого поля называется изменчивой записью. У изменчивой записи есть «семантика выпуска»; то есть это гарантированно произойдет после любых обращений к памяти до инструкции записи в последовательности инструкций. ECMA-334.

Ucho 05.01.2021 12:53

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

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

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

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

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