Подтвердить что ты не робот

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

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

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.

4b9b3361

Ответ 1

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

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

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

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

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

Ответ 2

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

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

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

Однако! Это должно быть не проблема:

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

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


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

Ответ 3

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

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

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

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

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

Интересно прочитать здесь и там некоторые неправильные выводы, но все-все-таки отлично по теме - это сообщение

Ответ 4

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

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

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

Ответ 5

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

(исключения - это блокированные функции с явной семантикой получения/выпуска)

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

Ответ 6

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

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

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