for - multithreading c# book
Implementación de Thread.VolatileRead (2)
Pensé que hasta hace poco. Las lecturas volátiles no son lo que crees que son, no se trata de garantizar que obtengan el valor más reciente; se trata de asegurarse de que ninguna lectura que se encuentra más adelante en el código del programa se mueva antes de esta lectura. Eso es lo que la especificación garantiza, y al igual que para las escrituras volátiles, garantiza que no se mueva ninguna escritura anterior después de la volátil.
No estás solo sospechando este código, pero Joe Duffy lo explica mejor que yo :)
Mi respuesta a esto es renunciar a la codificación sin bloqueo que no sea el uso de elementos como PFX, que están diseñados para aislarme de él. El modelo de memoria es demasiado difícil para mí, se lo dejo a los expertos y me quedo con las cosas que sé que son seguras.
Un día actualizaré mi artículo de subprocesos para reflejar esto, pero creo que primero necesito poder discutirlo con mayor sensatez ...
(No sé acerca de la parte de no alineación, por cierto. Sospecho que la incorporación podría introducir algunas otras optimizaciones que no están destinadas a ocurrir en las lecturas / escrituras volátiles, pero podría estar equivocado fácilmente ...)
Estoy mirando la implementación de los métodos VolatileRead / VolatileWrite (usando Reflector), y estoy confundido por algo.
Esta es la implementación para VolatileRead:
[MethodImpl(MethodImplOptions.NoInlining)]
public static int VolatileRead(ref int address)
{
int num = address;
MemoryBarrier();
return num;
}
¿Cómo se coloca la barrera de la memoria después de leer el valor de "dirección"? ¿No se supone que es lo contrario? (lugar antes de leer el valor, por lo que cualquier escritura pendiente a "dirección" se completará en el momento en que realicemos la lectura real. Lo mismo se aplica a VolatileWrite, donde la barrera de la memoria se coloca antes de la asignación del valor. ¿Por qué? ? Además, ¿por qué estos métodos tienen el atributo NoInlining ? ¿Qué podría suceder si estuvieran en línea?
Tal vez estoy simplificando en exceso, pero creo que las explicaciones sobre la reordenación y la coherencia de la memoria caché proporcionan muchos detalles.
Entonces, ¿por qué el MemoryBarrier viene después de la lectura real? Trataré de explicar esto con un ejemplo que use object en lugar de int.
Uno puede pensar que lo correcto es: El subproceso 1 crea el objeto (inicializa sus datos internos). El hilo 1 pone el objeto en una variable. Luego "hace una cerca" y todos los hilos ven el nuevo valor.
Entonces, la lectura es algo como esto: El hilo 2 "hace una valla". Hilo 2 lee la instancia del objeto. El subproceso 2 está seguro de que tiene todos los datos internos de esa instancia (ya que comenzó con una cerca).
El mayor problema con esto es: El subproceso 1 crea el objeto y lo inicializa. El hilo 1 pone el objeto en una variable. Antes de que el subproceso descargue la memoria caché, la propia CPU vacía parte de la memoria caché ... confirma solo la dirección de la variable (no el contenido de esa variable).
En ese momento, Thread 2 ya había vaciado su caché. Así que va a leer todo desde la memoria principal. Entonces, lee la variable (está ahí). Luego lee el contenido (no está ahí).
Finalmente, después de todo esto, la CPU 1 ejecuta el subproceso 1 que hace la cerca.
Entonces, ¿qué pasa con la lectura y escritura volátil? La escritura volátil hace que el contenido del objeto vaya a la memoria inmediatamente (comienza por la cerca), luego establecen la variable (puede que no vaya inmediatamente a la memoria real). Luego, la lectura volátil primero borrará el caché. Entonces lee el campo. Si recibe un valor al leer el campo, es seguro que los contenidos apuntados por esa referencia realmente están allí.
Por esas pequeñas cosas, sí, es posible que hagas un VolatileWrite (1) y otro hilo aún vea el valor de cero. Pero tan pronto como otros subprocesos ven el valor de 1 (usando una lectura volátil), todos los demás elementos necesarios a los que se puede hacer referencia ya están allí. Realmente no se puede decir que, al leer el valor anterior (0 o nulo), es posible que no progrese, ya que todavía no tiene todo lo que necesita.
Ya vi algunas discusiones que, incluso si eso borra los cachés dos veces, el patrón correcto será:
MemoryBarrier: eliminará otras variables modificadas antes de esta llamada
Escribir
MemoryBarrier - garantizará que la escritura fue borrada
La Lectura necesitará entonces lo mismo:
MemoryBarrier
Leer - Garantiza que veamos la información más reciente ... tal vez una que se colocó DESPUÉS de nuestra barrera de memoria.
Como algo pudo aparecer después de nuestro MemoryBarrier y ya fue leído, debemos poner otro MemoryBarrier para acceder a los contenidos.
Esas podrían ser dos Escrituras o dos Escrituras si existiera en .Net.
No estoy seguro de todo lo que dije ... es una "compilación" de mucha información que obtuve y realmente explica por qué VolatileRead y VolatileWrite parecen estar invertidos, pero también garantiza que no se lean valores no válidos cuando se utilicen.