multithreading - variable - volatile java




¿Cómo entiendo las barreras de memoria de lectura y volátil (2)

Algunos idiomas proporcionan un modificador volatile que se describe como realizar una "barrera de memoria de lectura" antes de leer la memoria que respalda una variable.

Una barrera de memoria de lectura se describe comúnmente como una forma de asegurar 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, al utilizar esta definición, parece que todavía se podría leer un valor obsoleto. En otras palabras, realizar lecturas en un cierto orden no significa que deba consultarse la memoria principal u otras CPU para garantizar que los valores de lectura posteriores reflejen lo último en el sistema en el momento de la barrera de lectura o se escriban posteriormente después de leer la barrera.

Entonces, ¿volatile realmente garantiza que se lea un valor actualizado o simplemente (jadeo) que los valores que se leen estén al menos tan actualizados como los datos anteriores a la barrera? ¿O alguna otra interpretación? ¿Cuáles son las implicaciones prácticas de esta respuesta?


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

Las barreras no están ahí para controlar el "último" valor o la "frescura" de los valores. Están allí para controlar el orden relativo de los accesos de 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 ocurran". Aunque están en cola en orden, mientras que dentro de la cola, las escrituras se pueden reordenar. (Entonces, tal vez 'cola' no sea el mejor nombre ...) A menos que use las barreras de escritura para evitar la reordenación.

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

Esto es cierto independientemente de lo que el compilador intente hacer con respecto al orden del código generado. es decir, 'volatile' en C ++ no será de ayuda, ya que solo le dice al compilador que emita código para volver a leer el valor de "memory", NO le dice a la CPU cómo / dónde leerlo (es decir, "memory" es muchas cosas a nivel de la CPU).

Por lo tanto, las barreras de lectura / escritura ponen bloques para evitar que se vuelvan a ordenar en las colas de lectura / escritura (la lectura no suele ser una cola, pero los efectos de reordenación son los mismos).

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

Adquirir - p. Ej., Leer-adquirir (x) agregará la lectura de x en la cola de lectura y vaciará la cola (en realidad no vaciará la cola, sino que agregará un marcador que dice que no se debe reordenar nada antes de esta lectura, que es como si el la cola estaba enrojecida). Así que más tarde (en orden de código) las lecturas se pueden reordenar, pero no antes de la lectura de x.

Liberación: por ejemplo, write-release (x, 5) vaciará (o marcará) la cola primero, luego agregará la solicitud de escritura a la cola de escritura. Por lo tanto, las escrituras anteriores no se volverán a ordenar después de que 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 adquisición y escribo con la versión porque esto es típico, pero son posibles diferentes combinaciones.

Adquirir y Liberar se consideran 'medias barreras' o 'medias cercas' porque solo evitan que el reordenamiento sea de una manera.

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

Por lo general, para la programación libre de bloqueo, o C # o java 'volatile', lo que desea / necesita es lectura-adquisición y escritura-liberación.

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();
}

Entonces, en primer lugar, esta es una mala manera de programar subprocesos. Las cerraduras serían más seguras. Pero solo para ilustrar las barreras ...

Después de que el hilo A () termine de escribir foo, debe escribir foo-> ready LAST, realmente el último; de lo contrario, otros hilos podrían ver foo-> ready antes y obtener los valores incorrectos de x / y / z. Así que usamos write_release en foo-> ready, que, como se mencionó anteriormente, "vacía" la cola de escritura (asegurándose de que x, y, z están confirmados) luego agrega la solicitud ready = true a la cola. Y luego agrega la barra = 13 de solicitud. Tenga en cuenta que dado que acabamos de usar una barrera de liberación (no una barra completa), la barra = 13 se puede escribir antes de estar lista. ¡Pero no nos importa! es decir, estamos asumiendo que la barra no está cambiando los datos compartidos.

Ahora threadB () necesita saber que cuando decimos "listo" realmente queremos decir listo. Así que hacemos un read_acquire(foo->ready) . Esta lectura se agrega a la cola de lectura, ENTONCES la cola se vacía. Tenga en cuenta que w = some_global también puede estar en la cola. Así que foo-> ready puede leerse antes de some_global . Pero, de nuevo, no nos importa, ya que no es parte de los datos importantes que estamos teniendo tanto cuidado. Lo que sí nos importa es foo-> x / y / z. Por lo tanto, se agregan a la cola de lectura después de la obtención de color / marcador, garantizando que se lean solo después de leer foo-> ready.

Tenga en cuenta también que esto suele ser exactamente las mismas barreras que se utilizan para bloquear y desbloquear un mutex / CriticalSection / etc. (es decir, adquirir en bloqueo (), liberar en desbloqueo ()).

Asi que,

  • Estoy bastante seguro de que esto (es decir, adquisición / lanzamiento) es exactamente lo que los documentos de MS dicen que sucede para las lecturas / escrituras de variables "volátiles" en C # (y opcionalmente para MS C ++, pero esto no es estándar). Consulte http://msdn.microsoft.com/en-us/library/aa645755(VS.71).aspx incluye "Una lectura volátil tiene" adquisición de semántica "; es decir, se garantiza que ocurra antes de cualquier referencia a la memoria que ocurra despues de eso ... "

  • Creo que Java es el mismo, aunque no soy tan familiar. Sospecho que es exactamente lo mismo, porque simplemente no necesitas más garantías que leer-adquirir / escribir-liberar.

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

  • Y, por favor, tenga en cuenta que, como se mencionó, el reordenamiento se produce tanto en las lecturas como en las escrituras, así que solo usar una barrera en un hilo y no el otro NO FUNCIONARÁ es decir, un lanzamiento de escritura no es suficiente sin la lectura-adquisición. es decir, incluso si lo escribe en el orden correcto, podría leerse en el orden incorrecto si no usó 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 en realidad mucho más complicadas que eso, pero seguir con la adquisición / lanzamiento lo llevará bastante lejos.


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

En contraste, las barreras de memoria de la CPU se proporcionan directamente a través de las instrucciones de la CPU o implícitas con ciertas reglas de funcionamiento de ensamblador (como el prefijo de lock en x86) y se usan, por ejemplo, cuando se habla con dispositivos de hardware donde el orden de las lecturas y escrituras en los registros de E / S asignados a la memoria es importante o sincronizando el acceso a la memoria en un entorno de multiprocesamiento.

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

Here está uno de los cebadores en las barreras de memoria de la CPU.







memory-barriers