Concurrencia: atómica y volátil en el modelo de memoria C++11

Resuelto Abhijit-K asked hace 12 años • 4 respuestas

Una variable global se comparte entre 2 subprocesos que se ejecutan simultáneamente en 2 núcleos diferentes. Los hilos escriben y leen las variables. Para la variable atómica, ¿puede un hilo leer un valor obsoleto? Cada núcleo puede tener un valor de la variable compartida en su caché y cuando un subproceso escribe en su copia en un caché, el otro subproceso en un núcleo diferente puede leer un valor obsoleto de su propio caché. ¿O el compilador realiza una orden de memoria fuerte para leer el último valor del otro caché? La biblioteca estándar c++11 tiene soporte std::atomic. ¿En qué se diferencia de la palabra clave volátil? ¿Cómo se comportarán de manera diferente los tipos volátiles y atómicos en el escenario anterior?

Abhijit-K avatar Jan 11 '12 19:01 Abhijit-K
Aceptado

En primer lugar, volatileno implica acceso atómico. Está diseñado para cosas como E/S asignadas en memoria y manejo de señales. volatilees completamente innecesario cuando se usa con std::atomicy, a menos que su plataforma documente lo contrario, volatileno tiene relación con el acceso atómico o el orden de la memoria entre subprocesos.

Si tiene una variable global que se comparte entre subprocesos, como por ejemplo:

std::atomic<int> ai;

luego, las restricciones de visibilidad y orden dependen del parámetro de ordenación de la memoria que utilice para las operaciones y de los efectos de sincronización de bloqueos, subprocesos y accesos a otras variables atómicas.

En ausencia de sincronización adicional, si un subproceso escribe un valor, aino hay nada que garantice que otro subproceso verá el valor en un período de tiempo determinado. El estándar especifica que debe ser visible "en un período de tiempo razonable", pero cualquier acceso dado puede devolver un valor obsoleto.

El orden de memoria predeterminado de std::memory_order_seq_cstproporciona un orden total global único para todas std::memory_order_seq_cstlas operaciones en todas las variables. Esto no significa que no pueda obtener valores obsoletos, pero sí significa que el valor que obtiene determina y está determinado por dónde se encuentra su operación en este orden total.

Si tiene 2 variables compartidas xy y, inicialmente cero, y tiene un hilo que escribe 1 xy otro que escribe 2 y, entonces un tercer hilo que lea ambos puede ver (0,0), (1,0), (0,2). ) o (1,2) ya que no existe ninguna restricción de orden entre las operaciones y, por lo tanto, las operaciones pueden aparecer en cualquier orden en el orden global.

Si ambas escrituras son del mismo hilo, lo que hace x=1antes y=2y el hilo de lectura lee yantes de xentonces (0,2) ya no es una opción válida, ya que la lectura de y==2implica que la escritura anterior xes visible. Los otros 3 emparejamientos (0,0), (1,0) y (1,2) aún son posibles, dependiendo de cómo se intercalen las 2 lecturas con las 2 escrituras.

Si utiliza otros ordenamientos de memoria, como std::memory_order_relaxedo std::memory_order_acquire, las restricciones se relajan aún más y el ordenamiento global único ya no se aplica. Los subprocesos ni siquiera necesariamente tienen que ponerse de acuerdo en el orden de dos tiendas para separar variables si no hay sincronización adicional.

La única forma de garantizar que tiene el valor "más reciente" es utilizar una operación de lectura, modificación y escritura como exchange(), compare_exchange_strong()o fetch_add(). Las operaciones de lectura, modificación y escritura tienen una restricción adicional de que siempre operan con el valor "más reciente", por lo que una secuencia de ai.fetch_add(1)operaciones realizadas por una serie de subprocesos devolverá una secuencia de valores sin duplicados ni espacios. Sin embargo, en ausencia de restricciones adicionales, todavía no hay garantía de qué subprocesos verán qué valores. En particular, es importante tener en cuenta que el uso de una operación RMW no obliga a que los cambios de otros subprocesos se vuelvan visibles más rápido, solo significa que si el RMW no ve los cambios, todos los subprocesos deben aceptar que son posteriores. en el orden de modificación de esa variable atómica que la operación RMW. Los almacenes de diferentes subprocesos aún pueden retrasarse por cantidades de tiempo arbitrarias, dependiendo de cuándo la CPU realmente envía el almacenamiento a la memoria (en lugar de solo su propio búfer de almacenamiento), físicamente qué tan separadas están las CPU que ejecutan los subprocesos (en el caso de un sistema multiprocesador) y los detalles del protocolo de coherencia de caché.

Trabajar con operaciones atómicas es un tema complejo. Le sugiero que lea mucho material de antecedentes y examine el código publicado antes de escribir código de producción con atomics. En la mayoría de los casos, es más fácil escribir código que utiliza bloqueos y no es notablemente menos eficiente.

Anthony Williams avatar Jan 12 '2012 10:01 Anthony Williams

volatiley las operaciones atómicas tienen un trasfondo diferente y fueron introducidas con una intención diferente.

volatiledata de hace mucho tiempo y está diseñado principalmente para evitar optimizaciones del compilador al acceder a IO asignadas en memoria. Los compiladores modernos tienden a no hacer más que suprimir las optimizaciones para volatile, aunque en algunas máquinas, esto no es suficiente ni siquiera para las E/S asignadas en memoria. Excepto en el caso especial de los manejadores de señales, y setjmp, longjmpy getjmpsecuencias (donde el estándar C, y en el caso de señales, el estándar Posix, da garantías adicionales), debe considerarse inútil en una máquina moderna, donde sin instrucciones adicionales especiales (vallas o barreras de memoria), el hardware puede reordenar o incluso suprimir determinados accesos. Como no deberías usar setjmp et al. en C++, esto deja más o menos manejadores de señales, y en un entorno multiproceso, al menos en Unix, también hay mejores soluciones para ellos. Y posiblemente IO asignada en memoria, si está trabajando en código kernel y puede asegurarse de que el compilador genere lo que sea necesario para la plataforma en cuestión. (Según el estándar, volatileel acceso es un comportamiento observable, que el compilador debe respetar. Pero el compilador define lo que se entiende por "acceso", y la mayoría parece definirlo como "se ejecutó una instrucción de máquina de carga o almacenamiento". , en un procesador moderno, ni siquiera significa que haya necesariamente un ciclo de lectura o escritura en el bus, y mucho menos que esté en el orden esperado).

Dada esta situación, el estándar C++ agregó acceso atómico, que proporciona una cierta cantidad de garantías entre subprocesos; en particular, el código generado alrededor de un acceso atómico contendrá las instrucciones adicionales necesarias para evitar que el hardware reordene los accesos y para garantizar que los accesos se propaguen hasta la memoria global compartida entre los núcleos de una máquina multinúcleo. (En un momento del esfuerzo de estandarización, Microsoft propuso agregar esta semántica a volatile, y creo que algunos de sus compiladores de C++ lo hacen. Sin embargo, después de discutir las cuestiones en el comité, el consenso general, incluido el representante de Microsoft, fue que era es mejor dejar volatilesu significado original y definir los tipos atómicos). O simplemente usar las primitivas a nivel del sistema, como mutex, que ejecutan cualquier instrucción necesaria en su código. (Tienen que hacerlo. No se puede implementar un mutex sin algunas garantías sobre el orden de los accesos a la memoria).

James Kanze avatar Jan 11 '2012 13:01 James Kanze

Aquí hay una sinopsis básica de cuáles son las 2 cosas:

1) Palabra clave volátil:
le dice al compilador que este valor podría alterarse en cualquier momento y, por lo tanto, NUNCA debe almacenarlo en caché en un registro. Busque la antigua palabra clave "registro" en C. "Volatile" es básicamente el operador "-" para "registrar" el "+". Los compiladores modernos ahora realizan la optimización que "registro" solía solicitar explícitamente de forma predeterminada, por lo que ya solo ve "volátil". El uso del calificador volátil garantizará que su procesamiento nunca utilice un valor obsoleto, pero nada más.

2) Atómico:
las operaciones atómicas modifican los datos en un solo tic de reloj, por lo que es imposible que CUALQUIER otro hilo acceda a los datos en medio de dicha actualización. Por lo general, se limitan a las instrucciones de ensamblaje de un solo reloj que admita el hardware; cosas como ++,-- e intercambiar 2 punteros. Tenga en cuenta que esto no dice nada sobre el ORDEN en el que los diferentes subprocesos EJECUTARÁN las instrucciones atómicas, solo que nunca se ejecutarán en paralelo. Es por eso que tienes todas esas opciones adicionales para forzar un pedido.

Zack Yezek avatar Aug 02 '2014 02:08 Zack Yezek

Volátil y atómico tienen diferentes propósitos.

Volátil: informa al compilador para evitar la optimización. Esta palabra clave se utiliza para variables que cambiarán inesperadamente. Por lo tanto, se puede utilizar para representar los registros de estado del hardware, variables de ISR y variables compartidas en una aplicación multiproceso.

Atómico: También se utiliza en caso de aplicaciones multiproceso. Sin embargo, esto garantiza que no haya ningún bloqueo/bloqueo durante el uso en una aplicación multiproceso. Las operaciones atómicas están libres de razas y son indivisibles. Algunos de los escenarios clave de uso son verificar si un candado está libre o usado, agregar atómicamente el valor y devolver el valor agregado, etc. en una aplicación de subprocesos múltiples.

Karthik Balaguru avatar Oct 24 '2014 19:10 Karthik Balaguru