¿Interlocked.CompareExchange usa una barrera de memoria?

Estoy leyendo la publicación de Joe Duffy sobre lecturas y escrituras volátiles, y la puntualidad , y estoy tratando de comprender algo sobre la última muestra de código en la publicación:

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

Cuando se ejecuta la segunda operación CMPXCHG, ¿utiliza una barrera de memoria para asegurarse de que el valor de m_state sea ​​el último valor escrito en él? ¿O solo usará algún valor que ya esté almacenado en el caché del procesador? (suponiendo que m_state no está declarado como volátil).
Si entiendo correctamente, si CMPXCHG no va a usar una barrera de memoria, entonces el procedimiento de adquisición de locking completo no será justo ya que es muy probable que el hilo que fue el primero en adquirir el locking, sea el que adquirirá todo de los siguientes lockings . ¿Lo entendí correctamente o me estoy perdiendo algo aquí?

Editar : La pregunta principal es si llamar a CompareExchange causará una barrera de memoria antes de intentar leer el valor de m_state. Entonces, si todos los subprocesos asignan 0 serán visibles cuando intenten llamar de nuevo a CompareExchange.

Cualquier instrucción x86 que tenga prefijo de locking tiene una barrera de memoria completa . Como se muestra en la respuesta de Abel, las API entrelazadas * y los intercambios de comparación usan instrucciones lock cmpxchg como lock cmpxchg . Por lo tanto, implica valla de memoria.

Sí, Interlocked.CompareExchange usa una barrera de memoria.

¿Por qué? Porque los procesadores x86 lo hicieron. Del Volumen 3A de Intel : Guía de progtwigción del sistema, Parte 1 , Sección 7.1.2.2:

Para los procesadores de la familia P6, las operaciones bloqueadas serializan todas las operaciones pendientes de carga y almacenamiento (es decir, esperan a que se completen). Esta regla también es válida para los procesadores Pentium 4 e Intel Xeon, con una excepción. Las operaciones de carga que hacen referencia a tipos de memoria débilmente ordenados (como el tipo de memoria WC) pueden no serializarse.

volatile no tiene nada que ver con esta discusión. Esto es sobre operaciones atómicas; para admitir operaciones atómicas en la CPU, x86 garantiza que se completen todas las cargas y almacenes previos.

ref no respeta las reglas volatile habituales, especialmente en cosas como:

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

Aquí, RunMethod no garantiza detectar cambios externos en isDone aunque el campo subyacente ( myField ) sea volatile ; RunMethod no lo sabe, por lo que no tiene el código correcto.

¡Sin embargo! Esto no debería ser un problema:

  • si está usando Interlocked , entonces use Interlocked para todo acceso al campo
  • si está utilizando el lock , utilice el lock para acceder al campo

Sigue esas reglas y debería funcionar bien.


Re la edición; sí, ese comportamiento es una parte crítica de Interlocked . Para ser sincero, no sé cómo se implementa (barrera de memoria, etc., tenga en cuenta que son métodos de “Llamada interna”, así que no puedo verificar ;-p) – pero sí: las actualizaciones de un hilo serán inmediatamente visibles para todos los demás , siempre que utilicen los métodos Interlocked (de ahí mi punto anterior).

Parece que hay alguna comparación con las funciones de la API de Win32 con el mismo nombre, pero este hilo trata de la clase Interlocked C #. Desde su propia descripción, se garantiza que sus operaciones son atómicas. No estoy seguro de cómo eso se traduce en “barreras de memoria” como se menciona en otras respuestas aquí, pero juzga por ti mismo.

En los sistemas de uniprocesador, no ocurre nada especial, solo hay una instrucción:

 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 

Pero en sistemas multiprocesador, se usa un locking de hardware para evitar que otros núcleos accedan a los datos al mismo tiempo:

 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 

Una lectura interesante con algunas conclusiones erróneas aquí y allá, pero en general excelente sobre el tema es esta publicación de blog en CompareExchange .

Se garantiza que las funciones de enclavamiento bloqueen el bus y la CPU mientras resuelve los operandos. La consecuencia inmediata es que ningún interruptor de hilo, en su CPU u otro, interrumpirá la función de enclavamiento en el medio de su ejecución.

Dado que está pasando una referencia a la función c #, el código del ensamblador subyacente funcionará con la dirección del entero real, por lo que el acceso variable no se optimizará. Funcionará exactamente como se esperaba.

editar: Aquí hay un enlace que explica mejor el comportamiento de la instrucción asm: http://faydoc.tripod.com/cpu/cmpxchg.htm
Como puede ver, el bus se detiene forzando un ciclo de escritura, por lo que cualquier otro “hilo” (léase: otros núcleos de CPU) que intente usar el bus al mismo tiempo se colocaría en una cola de espera.

MSDN dice acerca de las funciones API de Win32: ” La mayoría de las funciones interconectadas proporcionan barreras de memoria completas en todas las plataformas de Windows

(las excepciones son funciones entrelazadas con semántica de adquisición / liberación explícita)

A partir de eso, concluiría que el enclavamiento de C # en tiempo de ejecución ofrece las mismas garantías, ya que están documentados con comportamientos idénticos en otros momentos (y resuelven las declaraciones de CPU intrínsecas en las plataformas que conozco). Desafortunadamente, con la tendencia de MSDN a colocar muestras en lugar de documentación, no se explica explícitamente.

De acuerdo con ECMA-335 (sección I.12.6.5):

5. Operaciones atómicas explícitas. La biblioteca de clases proporciona una variedad de operaciones atómicas en la clase System.Threading.Interlocked. Estas operaciones (por ejemplo, Incremento, Decremento, Intercambio y CompareExchange) realizan operaciones de adquisición / liberación implícitas .

Entonces, estas operaciones siguen el principio de menos asombro .