Использует ли Interlocked.CompareExchange барьер памяти?

Я читаю сообщение Джо Даффи о Volatile чтения и записи и своевременности, и я пытаюсь понять кое-что о последнем примере кода в сообщении:

while (Interlocked.CompareExchange(ref m_state, 1, 0) != 0) ;
m_state = 0;
while (Interlocked.CompareExchange(ref m_state, 1, 0) != 0) ;
m_state = 0;
… 

Когда выполняется вторая операция CMPXCHG, использует ли она барьер памяти, чтобы гарантировать, что значение m_state действительно является последним записанным в него значением? Или он просто будет использовать какое-то значение, которое уже хранится в кеше процессора? (при условии, что m_state не объявлен как изменчивый).
Если я правильно понимаю, если CMPXCHG не будет использовать барьер памяти, тогда вся процедура получения блокировки будет несправедливой, поскольку она очень высока. вероятно, что поток, который первым получил блокировку, будет тем, который получит все последующие блокировки. Я правильно понял, или я что-то здесь упускаю?

Изменить. Главный вопрос заключается в том, вызовет ли вызов CompareExchange барьер памяти перед попыткой чтения значения m_state. Итак, будет ли присвоение 0 видимым для всех потоков, когда они попытаются снова вызвать CompareExchange.


person Community    schedule 17.10.2009    source источник


Ответы (6)


Любая инструкция x86 с префиксом lock имеет барьер полной памяти. Как показано в ответе Абеля, API-интерфейсы Interlocked * и CompareExchanges используют инструкцию с префиксом lock, такую ​​как lock cmpxchg. Значит, подразумевается забор памяти.

Да, Interlocked.CompareExchange использует барьер памяти.

Почему? Потому что это сделали процессоры x86. Из Тома 3A: Руководство по системному программированию, часть 1, раздел 7.1.2.2. :

Для процессоров семейства P6 заблокированные операции сериализуют все невыполненные операции загрузки и сохранения (то есть дождаться их завершения). Это правило также справедливо для процессоров Pentium 4 и Intel Xeon, за одним исключением. Операции загрузки, которые ссылаются на слабо упорядоченные типы памяти (такие как тип памяти WC), не могут быть сериализованы.

volatile не имеет никакого отношения к этому обсуждению. Речь идет об атомарных операциях; для поддержки атомарных операций в ЦП x86 гарантирует выполнение всех предыдущих загрузок и сохранений.

person minjang    schedule 11.11.2009
comment
Стоит отметить, что это ПОЛНЫЙ ЗАБОР, а не полузабор. - person Royi Namir; 08.09.2015
comment
Верно ли это для Interlocked.CompareExchange на ARM / AArch64, или это всего лишь деталь реализации C # для x86, которая не является частью гарантий стандарта языка? - person Peter Cordes; 17.06.2020

ref не соблюдает обычные volatile правила, особенно в таких вещах, как:

volatile bool myField;
...
RunMethod(ref myField);
...
void RunMethod(ref bool isDone) {
    while(!isDone) {} // silly example
}

Здесь RunMethod не гарантирует обнаружение внешних изменений в isDone, даже если базовое поле (myField) равно volatile; RunMethod не знает об этом, поэтому у него нет правильного кода.

Тем не мение! Это не должно быть проблемой:

  • если вы используете Interlocked, используйте Interlocked для всего доступа к полю
  • если вы используете lock, используйте lock для всего доступа к полю

Следуйте этим правилам, и все должно работать нормально.


Повторите редактирование; да, это поведение является важной частью Interlocked. Честно говоря, я не знаю, как это реализовано (барьер памяти и т. Д. - обратите внимание, что это методы "InternalCall", поэтому я не могу проверить ;-p) - но да: обновления из одного потока будут немедленно видны для все остальные до тех пор, пока они используют Interlocked методы (отсюда и моя точка зрения выше).

person Marc Gravell    schedule 17.10.2009
comment
Я не спрашиваю о volatiles, но только если Interlocked.Exchange необходим при снятии блокировки (или Thread.VolatileWrite будет более подходящим). и единственная проблема, которая может возникнуть из-за этого кода, - это привычка к несправедливости (как Джо упоминает в начале этого поста) - person ; 17.10.2009
comment
@Marc: источник методов InternalCall можно просмотреть (по большей части) через интерфейс командной строки общего источника SSCLI, также известный как Rotor. Interlocked.CompareExchange объясняется в этом интересном чтении: moserware.com /2008/09/how-do-locks-lock.html - person Abel; 10.11.2009

Кажется, есть какое-то сравнение с одноименными функциями Win32 API, но этот поток посвящен классу C # Interlocked. Из самого его описания гарантируется, что его операции атомарны. Я не уверен, как это переводится в «полные барьеры памяти», как упоминалось в других ответах здесь, но судите сами.

В однопроцессорных системах ничего особенного не происходит, есть всего одна инструкция:

FASTCALL_FUNC CompareExchangeUP,12
        _ASSERT_ALIGNEDInterlockedX86 ecx
        mov     eax, [esp+4]    ; Comparand
        cmpxchg [ecx], edx
        retn    4               ; result in EAX
FASTCALL_ENDFUNC CompareExchangeUP

Но в многопроцессорных системах аппаратная блокировка используется для предотвращения одновременного доступа к данным другим ядрам:

FASTCALL_FUNC CompareExchangeMP,12
        _ASSERT_ALIGNEDInterlockedX86 ecx
        mov     eax, [esp+4]    ; Comparand
  lock  cmpxchg [ecx], edx
        retn    4               ; result in EAX
FASTCALL_ENDFUNC CompareExchangeMP

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

Обновление для ARM

Как часто бывает, ответ будет: «в зависимости от обстоятельств». Похоже, что до 2.1 ARM имела полубарьер. В версии 2.1 это поведение было изменено на полный барьер для Interlocked операций.

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

person Abel    schedule 10.11.2009
comment
Да, это должно быть на x86, но является ли это также полным препятствием на ARM или AArch64, где оборудование может выполнять слабоупорядоченный атомарный RMW? - person Peter Cordes; 17.06.2020
comment
@PeterCordes, я ответил на этот вопрос в 2009 году. Версии .NET для ARM тогда не существовало, все были x86 / x64 (и, возможно, PowerPC). Но поскольку .NET теперь является полностью открытым исходным кодом, проверить и Mono, и RyuJIT тривиально. - person Abel; 19.06.2020
comment
Я нашел этот ответ, когда пытался проверить; это одна из вещей, которые возникли в Google. Я искал задокументированные гарантии, поэтому проверка исходного кода была бы не лучшим вариантом. (И я бы не назвал тривиальным. Вероятно, прямолинейный, но, вероятно, требующий много времени.) Этот ответ подходит для 2009 года, согласен; Я хотел сказать, что это не так полезно, как могло бы быть для нынешних читателей. Оказывается, другой ответ на этот вопрос ссылается на стандарт доказательства того, что заблокированные операции по крайней мере являются приобретением / выпуском. - person Peter Cordes; 19.06.2020
comment
@PeterCordes, ваш комментарий меня заинтересовал. Я проверил реальную реализацию и обсуждения на Github, и оказалось, что это зависит от версии. Я обновил свой ответ, включив это, не стесняйтесь редактировать мой ответ дальше, если найдете дополнительную информацию по этому вопросу. - person Abel; 19.06.2020

MSDN говорит о функциях Win32 API: " Большинство взаимосвязанных функций обеспечивают полную защиту памяти на всех платформах Windows "

(исключение составляют функции Interlocked с явной семантикой Acquire / Release)

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

person peterchen    schedule 17.10.2009

Связанные функции гарантированно остановят шину и процессор, пока он разрешает операнды. Непосредственным следствием этого является то, что никакое переключение потоков на вашем или другом процессоре не прервет заблокированную функцию в середине ее выполнения.

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

edit: Вот ссылка, которая лучше объясняет поведение инструкции asm: http://faydoc.tripod.com/cpu/cmpxchg.htm
Как видите, шина останавливается из-за принудительного цикла записи, поэтому любые другие «потоки» (читай: другие ядра процессора), которые будут пытаться использовать шину при этом будут помещены в очередь ожидания.

person Blindy    schedule 17.10.2009
comment
На самом деле, верно обратное (частично). Interlocked выполняет атомарную операцию и использует инструкцию сборки cmpxchg. Для этого не требуется переводить другие потоки в состояние ожидания, поэтому он очень эффективен. См. Раздел Inside InternalCall на этой странице: moserware.com/2008/ 09 / how-do-locks-lock.html - person Abel; 10.11.2009
comment
В современных процессорах нет разделяемой шины; lock cmpxchg на выровненном значении может просто заставить это ядро ​​ЦП отложить ответ на запросы MESI, аннулирующие / разделяющие, то есть блокировку кеша, а не блокировку шины. В любом случае, это говорит нам только о x86, а не о C # в целом для других ISA. - person Peter Cordes; 17.06.2020

Согласно ECMA-335 (раздел I.12.6.5):

5. Явные атомарные операции. Библиотека классов предоставляет множество атомарных операций в классе System.Threading.Interlocked. Эти операции (например, Increment, Decrement, Exchange и CompareExchange) выполняют неявные операции получения / освобождения.

Итак, эти операции следуют принципу наименьшего удивления.

person Valery Petrov    schedule 27.07.2017