2009-11-24 5 views
49

? Algunos lenguajes proporcionan un modificador volatile que se describe como una "barrera de lectura de memoria" antes de leer la memoria que respalda una variable.¿Cómo entiendo las barreras de lectura de memoria y el volátil

Una barrera de memoria de lectura se describe comúnmente como una forma de garantizar que la CPU haya realizado las lecturas solicitadas antes de la barrera antes de realizar una lectura solicitada después de la barrera. Sin embargo, usando esta definición, parecería que todavía se puede leer un valor obsoleto. En otras palabras, realizar lecturas en un orden determinado no parece significar que se debe consultar la memoria principal u otras CPU para garantizar que los valores posteriores leídos realmente reflejen lo último en el sistema en el momento de la barrera de lectura o que se escriban posteriormente. leer la barrera.

Por lo tanto, ¿realmente volátil garantiza que se lea un valor actualizado o simplemente (¡jadeó!) Que los valores que se leen están al menos tan actualizados como las lecturas anteriores a la barrera. ¿O alguna otra interpretación? ¿Cuáles son las implicaciones prácticas de esta respuesta?

Respuesta

93

Hay barreras de lectura y barreras de escritura; adquiere barreras y libera barreras. Y más (io vs memoria, etc.).

Las barreras no están ahí para controlar el "último" valor o "frescura" de los valores. Están ahí para controlar el orden relativo de los accesos a la memoria.

Las barreras de escritura controlan el orden de las escrituras. Debido a que las escrituras en la memoria son lentas (en comparación con la velocidad de la CPU), generalmente hay una cola de solicitud de escritura donde las escrituras se publican antes de que realmente sucedan. Aunque están en cola en orden, mientras están dentro de la cola, las escrituras pueden reordenarse. (Así que tal vez 'cola' no es el mejor nombre ...) A menos que use barreras de escritura para evitar el reordenamiento.

Las barreras de lectura controlan el orden de las lecturas. Debido a la ejecución especulativa (la CPU mira hacia adelante y carga desde la memoria al principio) y debido a la existencia del búfer de escritura (la CPU leerá un valor del búfer de escritura en lugar de memoria si está allí, es decir, la CPU cree que solo escribió X = 5, entonces ¿por qué volver a leerlo, solo ver que todavía está esperando convertirse en 5 en el búfer de escritura? Las lecturas pueden estar desordenadas.

Esto es cierto independientemente de lo que el compilador intente hacer con respecto al orden del código generado. es decir, "volátil" en C++ no ayudará aquí, porque solo le dice al compilador que debe sacar el código para volver a leer el valor de "memoria", NO le dice a la CPU cómo/dónde leerlo (es decir, "memoria"). hay muchas cosas en el nivel de la CPU).

Las barreras de lectura/escritura colocan bloques para evitar el reordenamiento en las colas de lectura/escritura (la lectura no suele ser tan larga, pero los efectos de reordenación son los mismos).

¿Qué tipo de bloques? - adquirir y/o liberar bloques.

Adquirir - por ejemplo, lectura adquirir (x) se sumará la lectura de x en la lectura de cola y vaciar la cola (no realmente vaciar la cola, además de añadir un marcador diciendo que no reordenar nada antes de esta lectura , que es como si la cola estuviera enrojecida). De modo que más tarde (en orden de código) las lecturas pueden reordenarse, pero no antes de la lectura de x.

Liberación - p. Ej., Escritura-liberación (x, 5) vaciará (o marcará) la cola primero, luego agregará la solicitud de escritura a la cola de escritura. Así que las escrituras anteriores no se reordenarán después de x = 5, pero tenga en cuenta que las escrituras posteriores se pueden reordenar antes de x = 5.

Tenga en cuenta que emparejé la lectura con adquirir y escribir con release porque esto es típico, pero diferentes combinaciones son posibles.

Adquirir y liberar se consideran "medias barreras" o "medias vallas" porque solo evitan que la reordenación se realice en una dirección.

Una barrera completa (o valla completa) aplica tanto una adquisición como una liberación, es decir, sin reordenamiento.

Normalmente para programación sin bloqueos, o C# o java 'volátil', lo que necesita/necesita es read-acquire y write-release.

es decir

void threadA() 
{ 
    foo->x = 10; 
    foo->y = 11; 
    foo->z = 12; 
    write_release(foo->ready, true); 
    bar = 13; 
} 
void threadB() 
{ 
    w = some_global; 
    ready = read_acquire(foo->ready); 
    if (ready) 
    { 
     q = w * foo->x * foo->y * foo->z; 
    } 
    else 
     calculate_pi(); 
} 

Así, en primer lugar, esta es una mala forma de hilos de programa. Las cerraduras estarían más seguras. Pero solo para ilustrar las barreras ...

Después de que threadA() termine escribiendo foo, necesita escribir foo-> listo LAST, realmente último, de lo contrario, otros subprocesos podrían ver foo-> listo antes y obtener los valores incorrectos de x/y/z. Así que usamos un write_release en foo-> listo, que, como se mencionó anteriormente, 'enjuaga' efectivamente la cola de escritura (asegurando que x, y, z están comprometidos) y luego agrega la solicitud ready = true a la cola. Y luego agrega la solicitud bar = 13. Tenga en cuenta que dado que acabamos de utilizar una barrera de liberación (no una barra llena), la barra = 13 puede escribirse antes de estar listo. ¡Pero no nos importa! es decir, asumimos que la barra no está cambiando los datos compartidos.

Ahora threadB() necesita saber que cuando decimos 'listo' realmente queremos decir listo. Entonces hacemos un read_acquire(foo->ready). Esta lectura se agrega a la cola de lectura, ENTONCES se vacía la cola. Tenga en cuenta que w = some_global también puede estar en la cola. Así que foo-> listo se puede leer antes desome_global. Pero nuevamente, no nos importa, ya que no es parte de los datos importantes sobre los que estamos siendo tan cuidadosos. Lo que sí nos importa es foo-> x/y/z. Por lo tanto, se agregan a la cola de lectura después de adquirir flush/marker, lo que garantiza que solo se leen después de leer foo-> ready.

Tenga en cuenta también que esta suele ser la misma barrera utilizada para bloquear y desbloquear un mutex/CriticalSection/etc. (es decir, adquirir al bloquear(), liberar al desbloquear()).

Así,

  • estoy bastante seguro de que esto (es decir, adquieren/liberación) es exactamente lo documentos de MS dicen que sucede para lectura/escribe de variables 'volátil' en C# (y opcionalmente para MS C++, pero esto no es estándar). Ver http://msdn.microsoft.com/en-us/library/aa645755(VS.71).aspx incluyendo "Una lectura volátil tiene 'adquirir la semántica', es decir, se garantiza que se produzca antes de cualquier referencia a la memoria que se producen después de que ..."

  • Yo creo java es lo mismo, aunque No estoy tan familiarizado. Sospecho que es exactamente lo mismo, porque simplemente no necesita más garantías que read-acquire/write-release.

  • En su pregunta usted estaba en el camino correcto al pensar que realmente se trata de un orden relativo: acaba de hacer los pedidos al revés (es decir, "los valores que se leen son al menos tan actualizados como el ¿lee antes de la barrera? "- no, las lecturas anteriores a la barrera no son importantes, su lectura DESPUÉS de la barrera que se garantiza que será DESPUÉS, viceversa para las escrituras).

  • Y tenga en cuenta que, como se mencionó, el reordenamiento ocurre tanto en las lecturas como en las escrituras, por lo que solo usar una barrera en un hilo y no en el otro NO FUNCIONARÁ. es decir, un write-release no es suficiente sin read-acquire. es decir, incluso si lo escribe en el orden correcto, podría leerse en el orden incorrecto si no utilizó las barreras de lectura para ir con las barreras de escritura.

  • Y, por último, tenga en cuenta que la programación sin bloqueos y las arquitecturas de memoria de la CPU pueden ser mucho más complicadas que eso, pero seguir con la adquisición/liberación le llevará bastante lejos.

+1

¿Importa que write_release y read_acquire hagan referencia a la misma variable lista? ¿O podrías usar variables dummmy separadas para ambos? El valor pasado parece no tener ningún propósito. –

+2

Es esencial usar la misma variable para los hilos que intentan sincronizarse. Al igual que necesita usar el mismo mutex o bloqueo en el enhebrado normal. En mi ejemplo threadA/B, queremos asegurarnos de que foo-> x, y, z se escriba antes de foo-> ready (de lo contrario, alguien podría ver "ready == true" antes de que foo estuviera realmente listo). En el lado de la lectura, no quiere leer x, y, z antes de que esté listo, por lo que necesita read_acquire en foo-> listo para asegurarse de que la CPU no reordena x, y, z lee antes ' if (foo-> listo) '. Si sus barreras estaban en diferentes variables ficticias, no tendría un punto de sincronización. – tony

8

volatile en la mayoría de los lenguajes de programación no implica una CPU real leer barrera de memoria, pero una orden para que el compilador no para optimizar las lecturas a través de almacenamiento en caché en un registro. Esto significa que el proceso/hilo de lectura obtendrá el valor "eventualmente". Una técnica común es declarar un indicador booleano volatile que se establecerá en un controlador de señal y se marcará en el bucle principal del programa.

En contraste, las barreras de memoria de CPU se proporcionan directamente a través de instrucciones de la CPU o implícitas con ciertos mnemónicos de ensamblador (como lock prefijo en x86) y se usan por ejemplo cuando se habla con dispositivos de hardware donde lee y escribe. registros es importante o sincronizar el acceso a la memoria en el entorno de multiprocesamiento.

Para responder a su pregunta, no, la barrera de memoria no garantiza el "último" valor, pero garantiza el orden de las operaciones de acceso a la memoria. Esto es crucial, por ejemplo, en la programación lock-free.

Here es uno de los primers en las barreras de memoria de la CPU.

+0

Soy consciente de que este es el caso en muchas implementaciones de C y C++. Mi pregunta es más relevante para plataformas de máquinas virtuales como Java y .NET. –

+0

Para lenguajes basados ​​en VM como Java y C#, necesita averiguar cuál es su "modelo de memoria". –

+0

Tenga en cuenta que 'volátil' no es suficiente, debe usar' volatile sig_atomic_t' para el uso conforme a las normas en un manejador de señal. – Jed

Cuestiones relacionadas