2009-10-17 4 views
18

Я читаю пост Джо Даффи о Volatile reads and writes, and timeliness, и я пытаюсь понять что-то о последнем примере кода в пост:Использует ли 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.

ответ

22

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

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

Почему? Потому что процессоры x86 сделали это. Из раздела Intel, Volume 3A: System Programming Guide Part 1, 7.1.2.2:

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

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

+0

Стоит упомянуть, что он обеспечивает FULL FENCE, а не половину забора. –

10

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 для всех доступа к поле

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


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

+0

Я не спрашиваю об летучих, но только если требуется блокировка. Обмен необходим при отпускании блокировки (или Thread.VolatileWrite будет более подходящим). , и единственная проблема, которая может возникнуть из этого кода, - это привычка к «несправедливости» (как упоминает Джо в начале этого сообщения) – 2009-10-17 08:28:18

+0

@Marc: источник методов InternalCall можно просмотреть (по большей части) через Общий источник CLI SSCLI, также известный как Ротор. Interlocked.CompareExchange объясняется в этом интересном чтении: http://www.moserware.com/2008/09/how-do-locks-lock.html – Abel

2

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

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

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

+0

Фактически, обратное (частично) верно. Блокировка выполняет атомарную операцию и использует инструкцию сборки 'cmpxchg'. Это не требует, чтобы другие потоки находились в состоянии ожидания, поэтому он очень эффективен. См. Раздел «Inside InternalCall» на этой странице: http://www.moserware.com/2008/09/how-do-locks-lock.html – Abel

2

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

(исключения блокировались функция с явной семантикой Приобретать/Release)

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

6

Похоже, что существует некоторое сравнение с функциями API Win32 с тем же именем, но этот поток относится ко всему классу C# 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 

Интересно, прочитав здесь и там некоторые неправильные выводы, но все-таки отлично по теме это blog post on CompareExchange.

0

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

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

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

Смежные вопросы