2012-01-11 59 views
38

Una variable global se comparte en 2 subprocesos simultáneos en 2 núcleos diferentes. Los hilos escriben y leen de 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 uno enhebra escrituras a su copia en un caché, el otro subproceso en un núcleo diferente puede leer el valor obsoleto de su propio caché. ¿O el compilador hace una fuerte ordenación de la memoria para leer el último valor de la otra caché? La biblioteca estándar de C++ 11 tiene std :: atomic support. ¿En qué se diferencia esto de la palabra clave volátil? ¿Cómo los tipos volátiles y atómicos se comportarán de manera diferente en el escenario anterior?Concurrencia: atómico y volátil en C++ 11 modelo de memoria

+20

Volátil es para leer desde unidades de cinta. Atomics son para concurrencia. Uno no tiene nada que ver con el otro. –

Respuesta

65

En primer lugar, volatile no implica el acceso atómica. Está diseñado para cosas como E/S mapeadas en memoria y manejo de señales. volatile es completamente innecesario cuando se usa con std::atomic, y salvo que su plataforma documente lo contrario, volatile no tiene relación con el acceso atómico o el orden de memoria entre subprocesos.

Si tiene una variable global que se comparte entre los hilos, tales como:

std::atomic<int> ai; 

entonces la visibilidad y las limitaciones de pedidos dependen del parámetro de ordenación de memoria que se utiliza para las operaciones y los efectos de sincronización de cerraduras, hilos y accesos a otras variables atómicas.

En ausencia de cualquier sincronización adicional, si un hilo escribe un valor en ai, entonces no hay nada que garantice que otro hilo verá el valor en un período de tiempo dado. La norma especifica que debe ser visible "en un período de tiempo razonable", pero cualquier acceso dado puede devolver un valor obsoleto.

El pedido de memoria predeterminado de std::memory_order_seq_cst proporciona un único pedido total global para todas las operaciones std::memory_order_seq_cst 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 el lugar en que se encuentra su operación en este orden total.

Si usted tiene 2 variables compartidas x y y, inicialmente cero, y tienen un hilo de escritura 1 a x y otro 2 a escribir y, a continuación, un tercer hilo que lee ambos pueden ver ya sea (0,0), (1 , 0), (0,2) o (1,2) ya que no existe una restricción de ordenamiento 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=1 antes y=2 y el hilo de lectura lee y antes x entonces (0,2) ya no es una opción válida, ya que la lectura de y==2 implica que la anterior escritura en x es visible. Los otros 3 emparejamientos (0,0), (1,0) y (1,2) siguen siendo posibles, dependiendo de cómo las 2 lecturas se entrelazan con las 2 escrituras.

Si usa otras ordenaciones de memoria como std::memory_order_relaxed o std::memory_order_acquire, las restricciones se relajan aún más, y el único pedido global ya no se aplica. Los hilos ni siquiera necesariamente tienen que acordar el pedido de dos tiendas para separar las variables si no hay una sincronización adicional.

La única manera de garantizar que tenga el "último" valor es utilizar una operación de lectura-modificación-escritura como exchange(), compare_exchange_strong() o fetch_add(). Las operaciones de lectura-modificación-escritura tienen una restricción adicional de que operan siempre en el "último" valor, por lo que una secuencia de operaciones de ai.fetch_add(1) por una serie de subprocesos devolverá una secuencia de valores sin duplicados o brechas. En ausencia de restricciones adicionales, todavía no hay garantía de qué hilos verán qué valores.

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

+1

Añadiría que la pregunta no está bien definida. Si un valor está obsoleto o no depende de la sincronización adicional. Si tiene una variable y un hilo sigue escribiéndole una secuencia de valores (por ejemplo, 1, 2 ...), y lee otro hilo, digamos 3, ¿el 3 es un valor obsoleto o no? Necesitas alguna otra confirmación de que de hecho 4 ya se ha escrito cuando lees 3. Pero eso requiere alguna otra observación que está en una relación de pasar antes con tu lectura de 3. Alguien debe haberte comunicado la lectura de 4 antes de leer o 3. Esto no sucederá con los accesos SC. –

+0

Todavía estoy luchando con el uso de volátiles con átomos: "volátil es completamente innecesario cuando se usa con 'std :: atomic'". ¿Qué pasa con la optimización de bucle de 'while (x.load (memory_order_relaxed));' => 'bool tmp = x.load (memory_order_relaxed); while (tmp); 'La norma es poco clara al respecto y Hans se convierte en diplomático cuando se le hace esta pregunta directamente ;-) –

+0

No es en conjunto, quiero decir. Volátil VS atómico. VS2005 pone una barrera de memoria HW para var variable y no necesita API enclavadas *. Atomic con memory_order_acquire haría lo mismo. En caso de volátil, ¿la llamada de salida OPS de lectura/escritura está en la memoria principal (DRAM)? Acerca de la sincronización adicional para leer valores de una variable compartida, supongamos que el hilo de escritura escribe y notifica las operaciones. El hilo de lectura WAIT y el currículum de notificar. Cuando se reanuda el hilo de lectura, ¿todavía es posible que el último valor aún esté en el caché del núcleo que ejecuta el hilo de escritura? Continúa debajo ... –

26

volatile y las operaciones atómicas tienen un fondo diferente, y se introdujeron con un intento diferente.

volatile data from way back, y está principalmente diseñado para evitar las optimizaciones del compilador al acceder a IO asignada a la memoria. Los compiladores modernos tienden a no hacer más que suprimir optimizaciones para volatile, aunque en algunas máquinas, esto no es suficiente incluso para la memoria asignada IO. Excepto para el caso especial de manejadores de señales, y setjmp, longjmp y getjmp secuencias (las que el nivel C, y en el caso de señales, el estándar Posix, da garantías adicionales), debe ser considera inútil en una máquina moderna , donde sin instrucciones adicionales especiales (vallas o barreras de memoria), el hardware puede reordenar o incluso suprimir ciertos accesos. Dado que no debe estar usando setjmp et al. en C++, esto deja más o menos a los manejadores de señal, y en un entorno multiproceso , al menos bajo Unix, hay mejores soluciones para esos también. Y posiblemente IO asignado a la memoria, si está trabajando en en código kernal y puede asegurarse de que el compilador genere lo que sea necesario para la plataforma en cuestión. (De acuerdo con la norma , volatile acceso es la conducta observable, que el compilador debe respetar. Pero el compilador llega a definir lo que se entiende por “ acceso ”, y la mayoría parece definirlo como “ una carga o instrucción de almacenamiento de la máquina fue ejecutado ”. lo cual, en un procesador moderno , ni siquiera significa que no es necesariamente una lectura o escritura ciclo en el autobús, y mucho menos que sea en el orden que usted espera.)

Ante esta situación, el acceso atómico estándar agregado de C++, que hace proporciona una cierta cantidad de garantías entre hilos s; en particular, el código generado alrededor de un acceso atómico contendrá las instrucciones adicionales necesarias para evitar que el hardware reordena los accesos , y para asegurar que los accesos se propaguen a la memoria global compartida entre núcleos en una máquina multinúcleo. (En un momento de el esfuerzo de estandarización, Microsoft propuso añadir esta semántica a volatile, y creo que algunos de sus compiladores de C++ hacer. Después de discusión de los temas en el comité, sin embargo, el general consenso — incluyendo el representante de Microsoft — era que era mejor dejar volatile con su significado original, y definir los tipos atómicos.) O simplemente use las primitivas de nivel del sistema, como los mutexes , que ejecutan las instrucciones necesarias en su código. (tienen que hacerlo. No se puede aplicar un mutex sin algunas garantías concerniente al orden de los accesos a memoria.)

+0

Creo que vale la pena enfatizar que los atómicos (así como las primitivas mutex) no pueden implementarse con algún tipo de soporte de hardware. (Por otro lado, 'volátil' es una mera referencia para el compilador). –

+8

Querías decir ** sin **, ¿verdad? –

+1

@KerrekSB No se puede implementar nada sin algún tipo de soporte de hardware :-). El lenguaje define la semántica (más o menos, en el caso de 'volátil'); depende del compilador generar lo que sea necesario. (Podría decirse que la intención de 'volátil' requeriría algunas instrucciones adicionales de la máquina en muchas máquinas, ya que el reordenamiento de acceso a la memoria en la CPU también afectaría la IO asignada a la memoria) –

2

Aquí es un resumen básico de lo que las 2 cosas son:

1) palabra clave volátil:
le dice al compilador que este valor podría alterar en cualquier momento y por lo tanto no debe NUNCA almacenar en caché en un registro. Busque la palabra clave "registrarse" anterior en C. "Volatile" es básicamente el operador "-" para "registrar" 's "+". Los compiladores modernos ahora hacen la optimización que "registra" solía solicitar explícitamente de manera predeterminada, por lo que ya solo ves "volátil". Usar el calificador volátil garantizará que su procesamiento nunca use un valor obsoleto, pero nada más.

2) Atomic:
Las operaciones atómicas modifican los datos en un solo tic del reloj, por lo que es imposible que CUALQUIER otro hilo acceda a los datos en el 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 intercambiando 2 punteros. Tenga en cuenta que esto no dice nada sobre el ORDEN los diferentes hilos ejecutarán las instrucciones atómicas, solo que nunca se ejecutarán en paralelo. Es por eso que tiene todas esas opciones adicionales para forzar un pedido.

+1

Las operaciones atómicas _ muy a menudo_ se refieren a un solo ciclo de reloj de hardware, pero afaik esto no es 100% garantizado/necesario, ni el único requisito (por ejemplo, muchas arquitecturas también requieren ciertas alineaciones). Uno siempre debe declarar la intención, no depender de 'bueno, funciona correctamente en la arquitectura X'. –

+0

Excepto que volátil puede forzar al compilador a poner una variable en un registro, para que no lo lea dos veces cuando el codificador solo dio instrucciones para leer una vez. –

3

Volatile y Atomic sirven para diferentes propósitos.

Volátil: Informa al compilador para evitar la optimización. Esta palabra clave se usa para las variables que deben cambiar inesperadamente. Por lo tanto, se puede usar para representar los registros de estado del hardware, las variables de ISR, las variables compartidas en una aplicación de subprocesos múltiples.

Atomic: También se utiliza en caso de aplicación de subprocesos múltiples. Sin embargo, esto garantiza que no haya bloqueo/bloqueo mientras se usa en una aplicación de subprocesos múltiples. Las operaciones atómicas son libres de razas e indivisibles. Pocos del escenario clave de uso es comprobar si un bloqueo es libre o usado, agregar atómicamente al valor y devolver el valor agregado, etc. en una aplicación de subprocesos múltiples.

Cuestiones relacionadas