2009-10-17 9 views
18

estoy leyendo el post de Joe Duffy sobre Volatile reads and writes, and timeliness, y estoy tratando de entender algo sobre el último ejemplo de código en el mensaje:¿Interlocked.CompareExchange utiliza una barrera de memoria?

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 es de hecho 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 utilizar una barrera de memoria, entonces el procedimiento de adquisición de bloqueo completo no será justo ya que es muy probable que el hilo que fue el primero en adquirir el bloqueo sea el que lo hará adquirir todos los siguientes cerraduras. ¿Lo entendí correctamente o me estoy perdiendo algo aquí?

Editar: La pregunta principal es en realidad 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.

Respuesta

22

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

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

¿Por qué? Porque los procesadores x86 lo hicieron. A partir de Intel Volume 3A: System Programming Guide Part 1, Sección 7.1.2.2:

Para los procesadores de la familia P6, operaciones cerradas serializar todas las operaciones de carga y almacenamiento sobresalientes (es decir, esperar a que se complete). 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.

+0

Vale la pena mencionar que proporciona FULL FENCE y no half fence. –

10

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

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

Aquí, RunMethod no está garantizada para detectar cambios externos a isDone pesar de que el campo subyacente (myField) es volatile; RunMethod no lo conoce, por lo que no tiene el código correcto.

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

  • si está utilizando Interlocked, a continuación, utilizar Interlocked para todo acceso al campo
  • si está utilizando lock, a continuación, utilizar lock para todo acceso a la campo

Siga estas reglas y debería funcionar correctamente.


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 y cuando utilizan los métodos Interlocked (de ahí mi punto anterior).

+0

No estoy preguntando sobre los volátiles, pero solo si es necesario un Interlocked.Exchange al liberar el bloqueo (o, Thread.VolatileWrite será más apropiado). y el único problema que podría surgir de este código es un hábito de "injusticia" (como Joe menciona al comienzo de esta publicación) –

+0

@Marc: la fuente de los métodos de llamada interna se puede ver (en su mayor parte) a través del Shared Source CLI SSCLI, también conocido como Rotor. El Interlocked.CompareExchange se explica en esta interesante lectura: http://www.moserware.com/2008/09/how-do-locks-lock.html – Abel

2

Las funciones de enclavamiento garantizan la detención del 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 el comportamiento de la instrucción asm mejor: http://faydoc.tripod.com/cpu/cmpxchg.htm
Como se puede ver, el autobús se ha detenido al forzar un ciclo de escritura, por lo que cualquier otro "hilos" (es decir: otros núcleos de CPU) que trataría de usar el autobús al mismo tiempo sería puesto en una fila de espera.

+0

En realidad, el reverso (parcialmente) es verdadero. Interlocked realiza una operación atómica y usa la instrucción de ensamblaje 'cmpxchg'. No requiere poner los otros hilos en estado de espera, por lo tanto, es muy eficiente. Ver la sección "Inside InternalCall" en esta página: http://www.moserware.com/2008/09/how-do-locks-lock.html – Abel

2

MSDN dice acerca de las funciones de la API de Win32: "La mayor parte de las funciones entrelazadas proporcionan barreras de memoria completos en todas las plataformas de Windows"

(las excepciones son funciones enclavado con explícitas semántica Adquirir/Release)

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 documentadas con comportamientos idénticos de otro modo (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.

6

Parece haber alguna comparación con las funciones de la API de Win32 con el mismo nombre, pero este hilo es todo sobre la clase C# Interlocked. 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 sistemas monoprocesador, no sucede nada especial, sólo hay una sola 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, un bloqueo de hardware se utiliza para evitar que otros núcleos de acceso 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 aquí y allá algunas conclusiones erróneas, pero en general excelente en el tema es este blog post on CompareExchange.

0

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 class. Estas operaciones (por ejemplo, Incremento, Decremento, Exchange y CompareExchange) realizan operaciones de adquisición/liberación implícita .

Por lo tanto, estas operaciones siguen principio de menos asombro.

Cuestiones relacionadas