.net multithreading thread-safety jit

¿Puede alguien proporcionar una explicación fácil de cómo se implementan ''Cercas completas'' en.Net usando Threading.MemoryBarrier?



multithreading thread-safety (2)

Soy claro en el uso de MemoryBarrier, pero no en lo que sucede entre bastidores en el tiempo de ejecución. ¿Alguien puede dar una buena explicación de lo que pasa?


En un modelo de memoria realmente fuerte, emitir instrucciones de cerco sería innecesario. Todos los accesos a la memoria se ejecutarían en orden y todas las tiendas serían visibles globalmente.

Las cercas de memoria son necesarias porque las arquitecturas comunes actuales no proporcionan un modelo de memoria sólido ; por ejemplo, x86 / x64 puede reordenar las lecturas en relación con las escrituras. (Una fuente más completa es el "Manual del desarrollador de software de arquitecturas Intel® 64 y IA-32 , 8.2.2 Orden de memoria en P6 y familias de procesadores más recientes" ). Como ejemplo de los gazillions, el algoritmo de Dekker fallará en x86 / x64 sin vallas.

Incluso si el JIT produce un código de máquina en el que las instrucciones con las cargas de memoria y los almacenes se colocan cuidadosamente, sus esfuerzos son inútiles si la CPU luego reordena estas cargas y almacena, lo que puede, siempre que se mantenga la ilusión de consistencia secuencial para la corriente. contexto / hilo.

Riesgo de simplificar en exceso: puede ayudar a visualizar las cargas y almacenes resultantes de la secuencia de instrucciones como un rebaño de animales salvajes. Al cruzar un puente estrecho (su CPU), nunca podrá estar seguro del orden de los animales, ya que algunos de ellos serán más lentos, otros más rápidos, algunos adelantados y otros se quedarán atrás. Si al inicio, cuando emites el código de la máquina, los divides en grupos colocando cercas infinitamente largas entre ellos, al menos puedes estar seguro de que el grupo A va antes que el grupo B.

Cercas aseguran el orden de las lecturas y escrituras. La redacción no es exacta, pero:

  • una cerca de la tienda "espera" a que todas las operaciones pendientes de la tienda (escritura) finalicen, pero no afecta a las cargas.
  • una cerca de carga "espera" a que todas las operaciones de carga (lectura) pendientes finalicen, pero no afecta a las tiendas.
  • una cerca completa "espera" a que finalicen todas las operaciones de almacenamiento y carga. Tiene el efecto de leer y escribir antes de que la cerca se ejecute antes de las escrituras y cargas que se encuentran en el "otro lado de la cerca" (más tarde que la cerca).

Lo que el JIT emite para una cerca completa, depende de la arquitectura (CPU) y de lo que garantiza el pedido de memoria que proporciona. Como el JIT sabe exactamente en qué arquitectura se ejecuta, puede emitir las instrucciones adecuadas.

En mi máquina x64, con .NET 4.0 RC, resulta ser un lock or .

int a = 0; 00000000 sub rsp,28h Thread.MemoryBarrier(); 00000004 lock or dword ptr [rsp],0 Console.WriteLine(a); 00000009 mov ecx,1 0000000e call FFFFFFFFEFB45AB0 00000013 nop 00000014 add rsp,28h 00000018 ret

Manual del desarrollador de software para arquitecturas Intel® 64 y IA-32 Capítulo 8.1.2:

  • "... las operaciones bloqueadas serializan todas las operaciones de carga y almacenamiento pendientes (es decir, espere a que se completen)". ... "Las operaciones bloqueadas son atómicas con respecto a todas las demás operaciones de memoria y todos los eventos visibles externamente. Sólo los accesos de búsqueda de tabla de página y de instrucción pueden pasar instrucciones bloqueadas. Las instrucciones bloqueadas se pueden usar para sincronizar datos escritos por un procesador y leer por otro procesador . "

  • Las instrucciones para ordenar la memoria abordan esta necesidad específica. MFENCE podría haberse utilizado como barrera total en el caso anterior (al menos en teoría, para una, las operaciones bloqueadas podrían ser más rápidas , para dos podría dar como resultado un comportamiento diferente ). MFENCE y sus amigos se pueden encontrar en el Capítulo 8.2.5 "Fortalecimiento o debilitamiento del modelo de pedido de memoria" .

Hay algunas formas más de serializar tiendas y cargas, aunque son poco prácticas o más lentas que los métodos anteriores:

  • En el capítulo 8.3 puede encontrar instrucciones completas de serialización como CPUID . Estas instrucciones de serialización también fluyen: "Nada puede pasar una instrucción de serialización y una instrucción de serialización no puede pasar ninguna otra instrucción (lectura, escritura, búsqueda de instrucciones o I / O)".

  • Si configura la memoria como no segura en caché (UC), le proporcionará un modelo sólido de memoria : no se permitirán accesos especulativos o fuera de orden y todos los accesos aparecerán en el bus, por lo tanto, no es necesario que emita una instrucción. :) Por supuesto, esto será un poco más lento de lo habitual.

...

Así que depende de Si hubiera una computadora con fuertes garantías de pedido, el JIT probablemente no emitiría nada.

IA64 y otras arquitecturas tienen sus propios modelos de memoria (y, por lo tanto, garantías de pedido de memoria (o falta de ellos)) y sus propias instrucciones / formas de lidiar con el almacenamiento de memoria / pedido de carga.


Mientras se realiza la programación concurrente sin bloqueo, uno debe preocuparse por la reordenación de las instrucciones del programa.

La reordenación de las instrucciones del programa puede ocurrir en varias etapas:

  1. Optimizaciones del compilador C # / VB.NET / F #
  2. Optimizaciones del compilador JIT
  3. Optimizaciones de CPU.

Las vallas de memoria son la única forma de garantizar el orden particular de las instrucciones de su programa. Básicamente, la valla de memoria es una clase de instrucciones que hace que la CPU aplique una restricción de orden. Las vallas de memoria se pueden poner en tres categorías:

  1. Cargar cercas: asegúrese de que las instrucciones de la CPU sin carga se muevan a través de las cercas
  2. Almacene cercas: asegúrese de que las instrucciones de la CPU no se muevan a través de las cercas
  3. Cercas completas: asegúrate de no cargar o almacenar las instrucciones de la CPU en las cercas

En .NET Framework hay muchas formas de emitir cercos: Interlock, Monitor, ReaderWriterLockSlim, etc.

Thread.MemoryBarrier emite una valla completa tanto en el compilador JIT como en el nivel del procesador.