¿Cuándo usar volátil con multihilo?

131

Si hay dos subprocesos que acceden a una variable global, muchos tutoriales dicen que la variable sea volátil para evitar que el compilador guarde en caché la variable en un registro y, por lo tanto, no se actualice correctamente. Sin embargo, dos hilos que acceden a una variable compartida es algo que requiere protección a través de un mutex, ¿no? Pero en ese caso, entre el bloqueo del hilo y la liberación del mutex, el código se encuentra en una sección crítica donde solo ese hilo puede acceder a la variable, en cuyo caso la variable no necesita ser volátil.

Entonces, ¿cuál es el uso / propósito de volátil en un programa multiproceso?

David Preston
fuente
3
En algunos casos, no desea / necesita protección mediante el mutex.
Stefan Mai
44
A veces está bien tener una condición de carrera, a veces no. ¿Cómo estás usando esta variable?
David Heffernan
3
@David: Un ejemplo de cuándo está "bien" tener una carrera, ¿por favor?
John Dibling
66
@John Aquí va. Imagine que tiene un hilo de trabajo que está procesando una serie de tareas. El subproceso de trabajo incrementa un contador cada vez que finaliza una tarea. El hilo maestro lee periódicamente este contador y actualiza al usuario con noticias del progreso. Mientras el contador esté correctamente alineado para evitar rasgaduras, no es necesario sincronizar el acceso. Aunque hay una carrera, es benigna.
David Heffernan
55
@John El hardware en el que se ejecuta este código garantiza que las variables alineadas no puedan sufrir rasgaduras. Si el trabajador está actualizando n a n + 1 a medida que el lector lee, al lector no le importa si obtiene n o n + 1. No se tomarán decisiones importantes, ya que solo se utiliza para informar sobre el progreso.
David Heffernan

Respuestas:

168

Respuesta corta y rápida : volatilees (casi) inútil para la programación de aplicaciones multiproceso independiente de la plataforma. No proporciona ninguna sincronización, no crea vallas de memoria ni garantiza el orden de ejecución de las operaciones. No hace operaciones atómicas. No hace que su código sea mágicamente seguro para subprocesos. volatilepuede ser la instalación más incomprendida en todo C ++. Vea esto , esto y esto para obtener más información sobrevolatile

Por otro lado, volatiletiene algún uso que puede no ser tan obvio. Se puede usar de la misma manera que se usaría constpara ayudar al compilador a mostrar dónde podría estar cometiendo un error al acceder a un recurso compartido de una manera no protegida. Alexandrescu analiza este uso en este artículo . Sin embargo, esto es básicamente usar el sistema de tipo C ++ de una manera que a menudo se ve como una invención y puede evocar un comportamiento indefinido.

volatilefue diseñado específicamente para ser utilizado al interactuar con hardware mapeado en memoria, manejadores de señal y la instrucción de código de máquina setjmp. Esto hace volatileque sea directamente aplicable a la programación a nivel de sistemas en lugar de la programación normal a nivel de aplicaciones.

El Estándar 2003 C ++ no dice que volatileaplique ningún tipo de semántica de Adquisición o Liberación en las variables. De hecho, el estándar es completamente silencioso en todos los asuntos de subprocesamiento múltiple. Sin embargo, las plataformas específicas aplican la semántica de adquisición y liberación en las volatilevariables.

[Actualización para C ++ 11]

El C ++ 11 estándar ahora hace acuse de multithreading directamente en el modelo de memoria y el lanuage, y proporciona instalaciones de la biblioteca de tratar con él de una manera independiente de plataforma. Sin embargo, la semántica de volatiletodavía no ha cambiado. volatileTodavía no es un mecanismo de sincronización. Bjarne Stroustrup dice lo mismo en TCPPPL4E:

No lo use, volatileexcepto en código de bajo nivel que trate directamente con hardware.

No asuma que volatiletiene un significado especial en el modelo de memoria. No es asi. No es, como en algunos idiomas posteriores, un mecanismo de sincronización. Para obtener sincronización, use atomic, a mutexo a condition_variable.

[/ Fin de actualización]

Todo lo anterior aplica el lenguaje C ++ en sí, según lo definido por el Estándar 2003 (y ahora el Estándar 2011). Sin embargo, algunas plataformas específicas agregan funcionalidad adicional o restricciones a lo que volatilehace. Por ejemplo, en MSVC 2010 (por lo menos) y liberan la semántica no se aplican a ciertas operaciones en volatilelas variables. Desde el MSDN :

Al optimizar, el compilador debe mantener el orden entre las referencias a objetos volátiles y las referencias a otros objetos globales. En particular,

Una escritura en un objeto volátil (escritura volátil) tiene semántica de lanzamiento; una referencia a un objeto global o estático que ocurre antes de una escritura en un objeto volátil en la secuencia de instrucciones ocurrirá antes de esa escritura volátil en el binario compilado.

Una lectura de un objeto volátil (lectura volátil) tiene semántica Adquirir; una referencia a un objeto global o estático que ocurre después de una lectura de memoria volátil en la secuencia de instrucciones ocurrirá después de esa lectura volátil en el binario compilado.

Sin embargo, puede tener en cuenta el hecho de que si sigue el enlace anterior, hay un debate en los comentarios sobre si la semántica de adquisición / liberación realmente se aplica en este caso.

John Dibling
fuente
19
Una parte de mí quiere rechazar esto debido al tono condescendiente de la respuesta y el primer comentario. "volátil es inútil" es similar a "la asignación manual de memoria es inútil". Si puede escribir un programa multiproceso sin volatileél es porque se colocó sobre los hombros de las personas que solían volatileimplementar bibliotecas de subprocesos.
Ben Jackson
20
@Ben solo porque algo desafía tus creencias no lo hace condescendiente
David Heffernan
39
@Ben: no, lee sobre lo que volatilerealmente hace en C ++. Lo que dijo @John es correcto , final de la historia. No tiene nada que ver con el código de la aplicación versus el código de la biblioteca, o "ordinarios" versus "programadores omniscientes divinos". volatilees innecesario e inútil para la sincronización entre hilos. Las bibliotecas de subprocesos no se pueden implementar en términos de volatile; tiene que depender de detalles específicos de la plataforma de todos modos, y cuando confía en ellos, ya no los necesita volatile.
jalf
66
@jalf: "lo volátil es innecesario e inútil para la sincronización entre hilos" (que es lo que usted dijo) no es lo mismo que "lo volátil es inútil para la programación multiproceso" (que es lo que dijo John en la respuesta). Estás 100% en lo correcto, pero no estoy de acuerdo con John (parcialmente): todavía se puede usar volátil para programación multiproceso (para un conjunto muy limitado de tareas)
44
@GMan: Todo lo que es útil solo es útil bajo un cierto conjunto de requisitos o condiciones. Volátil es útil para la programación multiproceso bajo un conjunto estricto de condiciones (y en algunos casos, incluso puede ser mejor (para alguna definición de mejor) que las alternativas). Dices "ignorando esto y ...", pero el caso cuando volátil es útil para multihilo no ignora nada. Inventaste algo que nunca reclamé. Sí, la utilidad de los volátiles es limitada, pero existe, pero todos podemos estar de acuerdo en que NO es útil para la sincronización.
31

(Nota del editor: en C ++ 11 volatileno es la herramienta adecuada para este trabajo y todavía tiene UB de carrera de datos. Úselo std::atomic<bool>con std::memory_order_relaxedcargas / tiendas para hacer esto sin UB. En implementaciones reales se compilará de la misma manera que volatile. Agregué una respuesta con más detalle, y también abordando las ideas erróneas en los comentarios de que la memoria débilmente ordenada podría ser un problema para este caso de uso: todas las CPU del mundo real tienen memoria compartida coherente, por volatilelo que funcionará para esto en implementaciones reales de C ++. No lo hagas.

Algunos comentarios en la discusión parece estar hablando sobre otros casos de uso donde se le necesita algo más fuerte que las atómicas relajados. Esta respuesta ya señala que volatileno le da orden).


Volátil es ocasionalmente útil por la siguiente razón: este código:

/* global */ bool flag = false;

while (!flag) {}

está optimizado por gcc para:

if (!flag) { while (true) {} }

Lo cual es obviamente incorrecto si el otro hilo escribe en la bandera. Tenga en cuenta que sin esta optimización, el mecanismo de sincronización probablemente funcione (dependiendo del otro código, pueden ser necesarias algunas barreras de memoria); no es necesario un mutex en el escenario 1 productor - 1 consumidor.

De lo contrario, la palabra clave volátil es demasiado extraña para ser utilizable: no proporciona ninguna garantía de ordenamiento de memoria con accesos tanto volátiles como no volátiles y no proporciona ninguna operación atómica, es decir, no obtiene ayuda del compilador con la palabra clave volátil, excepto el almacenamiento en caché de registros deshabilitado .

zeuxcg
fuente
44
Si mal no recuerdo, C ++ 0x atomic está destinado a hacer correctamente lo que mucha gente cree (incorrectamente) que hace volátil.
David Heffernan
14
volatileno evita que se reordenen los accesos a la memoria. volatilelos accesos no se reordenarán entre sí, pero no brindan ninguna garantía sobre el reordenamiento con respecto a los no volatileobjetos, por lo que, básicamente, también son inútiles como indicadores.
jalf
14
@Ben: Creo que lo tienes al revés. La multitud "volátil es inútil" se basa en el simple hecho de que el volátil no protege contra la reordenación , lo que significa que es completamente inútil para la sincronización. Otros enfoques pueden ser igualmente inútiles (como usted menciona, la optimización del código de tiempo de enlace puede permitir que el compilador eche un vistazo al código que supuso que el compilador trataría como una caja negra), pero eso no soluciona las deficiencias volatile.
jalf
15
@jalf: Vea el artículo de Arch Robinson (vinculado en otra parte de esta página), décimo comentario (por "Spud"). Básicamente, la reordenación no cambia la lógica del código. El código publicado usa el indicador para cancelar una tarea (en lugar de indicar que la tarea está hecha), por lo que no importa si la tarea se cancela antes o después del código (por ejemplo while (work_left) { do_piece_of_work(); if (cancel) break;}, si la cancelación se reordena dentro del ciclo, la lógica sigue siendo válida. Tenía un código que funcionaba de manera similar: si el hilo principal quiere terminar, establece el indicador para otros hilos, pero no ...
15
... importa si los otros subprocesos realizan algunas iteraciones adicionales de sus bucles de trabajo antes de que finalicen, siempre que suceda razonablemente poco después de que se establezca el indicador. Por supuesto, este es el ÚNICO uso en el que puedo pensar y es más bien nicho (y puede que no funcione en plataformas donde escribir en una variable volátil no hace que el cambio sea visible para otros hilos, aunque en al menos x86 y x86-64 esto trabajos). Ciertamente, no recomendaría a nadie que lo haga sin una muy buena razón, solo digo que una declaración general como "volátil nunca es útil en código multiproceso" no es 100% correcta.
16

En C ++ 11, normalmente nunca se usa volatilepara subprocesos, solo para MMIO

Pero TL: DR, "funciona" como atómico con mo_relaxedhardware con cachés coherentes (es decir, todo); es suficiente detener que los compiladores mantengan vars en registros. atomicno necesita barreras de memoria para crear atomicidad o visibilidad entre subprocesos, solo para hacer que el subproceso actual espere antes / después de una operación para crear un orden entre los accesos de este subproceso a diferentes variables. mo_relaxednunca necesita barreras, solo carga, almacena o RMW.

Para rodar sus propios elementos atómicos con volatile(y en línea asm para barreras) en los viejos tiempos malos antes de C ++ 11 std::atomic, volatileera la única buena manera de hacer que algunas cosas funcionen . Pero dependía de muchos supuestos sobre cómo funcionaban las implementaciones y nunca fue garantizado por ningún estándar.

Por ejemplo, el kernel de Linux todavía usa sus propios átomos atómicos volatile, pero solo admite algunas implementaciones específicas de C (GNU C, clang y quizás ICC). En parte, eso se debe a las extensiones GNU C y a la sintaxis y semántica en línea de asm, pero también porque depende de algunos supuestos sobre cómo funcionan los compiladores.

Casi siempre es la elección incorrecta para nuevos proyectos; puede usar std::atomic(con std::memory_order_relaxed) para obtener un compilador que emita el mismo código de máquina eficiente que podría con volatile. std::atomiccon mo_relaxedobsoletos volatilepara propósitos de enhebrado. (excepto tal vez para evitar errores de optimización perdidos atomic<double>en algunos compiladores ).

La implementación interna de std::atomiccompiladores convencionales (como gcc y clang) no solo se usa volatileinternamente; los compiladores exponen directamente las funciones integradas de carga atómica, almacenamiento y RMW. (por ejemplo, GNU C __atomicincorporados que operan en objetos "simples").


Volátil es utilizable en la práctica (pero no lo hagas)

Dicho esto, volatilese puede usar en la práctica para cosas como un exit_nowindicador en todas las implementaciones de C ++ existentes (?) En CPU reales, debido a cómo funcionan las CPU (cachés coherentes) y los supuestos compartidos sobre cómo volatiledebería funcionar. Pero no mucho más, y no es recomendable. El propósito de esta respuesta es explicar cómo funcionan realmente las CPU existentes y las implementaciones de C ++. Si no le importa eso, todo lo que necesita saber es que std::atomiccon mo_relaxed obsoletos volatilepara subprocesos.

(El estándar ISO C ++ es bastante vago, solo dice que los volatileaccesos deben evaluarse estrictamente de acuerdo con las reglas de la máquina abstracta C ++, no optimizados. Dado que las implementaciones reales usan el espacio de direcciones de memoria de la máquina para modelar el espacio de direcciones C ++, Esto significa que las volatilelecturas y las tareas deben compilarse para cargar / almacenar instrucciones para acceder a la representación de objetos en la memoria).


Como señala otra respuesta, un exit_nowindicador es un caso simple de comunicación entre subprocesos que no necesita ninguna sincronización : no está publicando que el contenido de la matriz esté listo ni nada de eso. Solo una tienda que se nota rápidamente por una carga no optimizada en otro hilo.

    // global
    bool exit_now = false;

    // in one thread
    while (!exit_now) { do_stuff; }

    // in another thread, or signal handler in this thread
    exit_now = true;

Sin volátil o atómico, la regla as-if y la suposición de que no hay carrera de datos UB permite que un compilador lo optimice en asm que solo verifica la bandera una vez , antes de ingresar (o no) un bucle infinito. Esto es exactamente lo que sucede en la vida real para los compiladores reales. (Y generalmente se optimiza mucho do_stuffporque el bucle nunca sale, por lo que cualquier código posterior que podría haber utilizado el resultado no es accesible si ingresamos al bucle).

 // Optimizing compilers transform the loop into asm like this
    if (!exit_now) {        // check once before entering loop
        while(1) do_stuff;  // infinite loop
    }

El programa de subprocesos múltiples se atascó en modo optimizado pero se ejecuta normalmente en -O0 es un ejemplo (con descripción de la salida asm de GCC) de cómo sucede exactamente esto con GCC en x86-64. También la programación MCU: la optimización de C ++ O2 se rompe mientras el bucle en la electrónica. SE muestra otro ejemplo.

Normalmente queremos optimizaciones agresivas que CSE y polipastos carguen de bucles, incluso para variables globales.

Antes de C ++ 11, volatile bool exit_nowera una forma de hacer que esto funcionara según lo previsto (en implementaciones normales de C ++). Pero en C ++ 11, el UB de carrera de datos todavía se aplica, por volatilelo que el estándar ISO no garantiza que funcione en todas partes, incluso suponiendo cachés coherentes HW.

Tenga en cuenta que para los tipos más anchos, volatileno garantiza la falta de rasgado. Ignoré esa distinción aquí boolporque no es un problema en las implementaciones normales. Pero eso también es parte de por qué volatiletodavía está sujeto a la carrera de datos UB en lugar de ser equivalente a atómica relajada.

Tenga en cuenta que "según lo previsto" no significa que el subproceso en exit_nowespera a que el otro subproceso salga realmente. O incluso que espera a que la exit_now=truetienda volátil sea ​​incluso visible globalmente antes de continuar con las operaciones posteriores en este hilo. ( atomic<bool>con el valor predeterminado mo_seq_cstlo haría esperar antes de que seq_cst se cargue por lo menos más tarde. En muchos ISA, simplemente obtendría una barrera completa después de la tienda).

C ++ 11 proporciona una forma no UB que compila lo mismo

Se debe usar una bandera "seguir corriendo" o "salir ahora" std::atomic<bool> flagconmo_relaxed

Utilizando

  • flag.store(true, std::memory_order_relaxed)
  • while( !flag.load(std::memory_order_relaxed) ) { ... }

le dará exactamente el mismo asm (sin instrucciones de barrera costosas) que obtendría volatile flag.

Además de no rasgar, atomictambién le permite almacenar en un hilo y cargar en otro sin UB, por lo que el compilador no puede levantar la carga de un bucle. (La suposición de que no hay carrera de datos UB es lo que permite las optimizaciones agresivas que queremos para los objetos no volátiles no atómicos). Esta característica de atomic<T>es más o menos la misma que volatilepara las cargas puras y las tiendas puras.

atomic<T>también haga, +=y así sucesivamente, operaciones atómicas de RMW (significativamente más caro que una carga atómica en un temporal, opere, luego una tienda atómica separada. Si no desea un RMW atómico, escriba su código con un temporal local).

Con el seq_cstpedido predeterminado que obtendría while(!flag), también agrega garantías de pedido wrt. accesos no atómicos y a otros accesos atómicos.

(En teoría, el estándar ISO C ++ no descarta la optimización del tiempo de compilación de los atómicos. Pero en la práctica los compiladores no lo hacen porque no hay forma de controlar cuándo eso no estaría bien. Hay algunos casos en los que incluso volatile atomic<T>no será suficiente control sobre la optimización de los compiladores atómicas si lo hicieron a optimizar, así que por ahora no lo hacen los compiladores. Ver ¿por qué no se fusionan compiladores std :: redundante escrituras atómicas? Tenga en cuenta que WG21 / p0062 desaconseja el uso volatile atomicde código actual para protegerse de la optimización de atomística.)


volatile en realidad funciona para esto en CPU reales (pero aún no lo uso)

incluso con modelos de memoria con un orden débil (no x86) . ¡Pero en realidad no lo use, use atomic<T>en su mo_relaxedlugar! El objetivo de esta sección es abordar las ideas erróneas sobre cómo funcionan las CPU reales, no justificarlas volatile. Si está escribiendo código sin bloqueo, probablemente le interese el rendimiento. Comprender los cachés y los costos de la comunicación entre subprocesos suele ser importante para un buen rendimiento.

Las CPU reales tienen memorias caché coherentes / memoria compartida: después de que una tienda de un núcleo se vuelve globalmente visible, ningún otro núcleo puede cargar un valor obsoleto. (Consulte también Myths Programmers Believe about CPU Caches, que habla un poco sobre los volátiles de Java, equivalente a C ++ atomic<T>con el orden de memoria seq_cst).

Cuando digo cargar , me refiero a una instrucción asm que accede a la memoria. Eso es lo que volatilegarantiza un acceso, y no es lo mismo que la conversión lvalue-to-rvalue de una variable C ++ no atómica / no volátil. (p . ej. local_tmp = flago while(!flag)).

Lo único que debe vencer son las optimizaciones en tiempo de compilación que no se vuelven a cargar después de la primera comprobación. Cualquier carga + verificación en cada iteración es suficiente, sin ningún pedido. Sin sincronización entre este subproceso y el subproceso principal, no tiene sentido hablar sobre cuándo ocurrió exactamente la tienda u ordenar la carga wrt. otras operaciones en el bucle. Solo cuando es visible para este hilo es lo que importa. Cuando vea el conjunto de banderas exit_now, saldrá. La latencia entre núcleos en un Xeon x86 típico puede ser algo así como 40ns entre núcleos físicos separados .


En teoría: subprocesos C ++ en hardware sin cachés coherentes

No veo ninguna manera de que esto pueda ser remotamente eficiente, con solo ISO C ++ puro sin requerir que el programador realice descargas explícitas en el código fuente.

En teoría, podría tener una implementación de C ++ en una máquina que no fuera así, que requiere descargas explícitas generadas por el compilador para hacer que las cosas sean visibles para otros hilos en otros núcleos . (O para que las lecturas no utilicen una copia quizás obsoleta). El estándar C ++ no lo hace imposible, pero el modelo de memoria de C ++ está diseñado para ser eficiente en máquinas coherentes de memoria compartida. Por ejemplo, el estándar C ++ incluso habla de "coherencia de lectura-lectura", "coherencia de lectura-escritura", etc. Una nota en el estándar incluso señala la conexión al hardware:

http://eel.is/c++draft/intro.races#19

[Nota: Los cuatro requisitos de coherencia anteriores no permiten efectivamente la reordenación del compilador de operaciones atómicas a un solo objeto, incluso si ambas operaciones son cargas relajadas. Esto efectivamente hace que la garantía de coherencia de caché proporcionada por la mayoría del hardware esté disponible para las operaciones atómicas de C ++. - nota final]

No hay un mecanismo para que una releasetienda solo se vacíe y algunos rangos de direcciones selectos: tendría que sincronizar todo porque no sabría qué otros hilos querrían leer si su carga de adquisición viera esta tienda de lanzamiento (formando un La secuencia de lanzamiento que establece una relación de antes de pasar a través de subprocesos, lo que garantiza que las operaciones no atómicas anteriores realizadas por el subproceso de escritura ahora sean seguras de leer. ser realmente inteligente para demostrar que solo unas pocas líneas de caché necesitaban vaciarse.

Relacionado: mi respuesta en ¿Es seguro mov + mfence en NUMA? entra en detalles sobre la inexistencia de sistemas x86 sin memoria compartida coherente. También relacionado: Reordenamiento de cargas y tiendas en ARM para obtener más información sobre cargas / tiendas en la misma ubicación.

No son Creo que las agrupaciones con memoria compartida no coherente, pero no son máquinas de sistema de una sola imagen. Cada dominio de coherencia ejecuta un núcleo separado, por lo que no puede ejecutar hilos de un solo programa C ++ a través de él. En su lugar, ejecuta instancias separadas del programa (cada una con su propio espacio de direcciones: los punteros en una instancia no son válidos en la otra).

Para que se comuniquen entre sí a través de descargas explícitas, normalmente usaría MPI u otra API de transmisión de mensajes para hacer que el programa especifique qué rangos de direcciones necesitan enjuague.


El hardware real no se ejecuta a std::threadtravés de los límites de coherencia de caché:

Existen algunos chips ARM asimétricos, con espacio de direcciones físicas compartidas pero no dominios de caché compartibles en el interior. Entonces no es coherente. (por ejemplo, un hilo de comentarios con un núcleo A8 y un Cortex-M3 como TI Sitara AM335x).

Pero se ejecutarían diferentes núcleos en esos núcleos, no una sola imagen del sistema que pudiera ejecutar hilos en ambos núcleos. No conozco ninguna implementación de C ++ que ejecute std::threadhilos a través de núcleos de CPU sin cachés coherentes.

Para ARM específicamente, GCC y clang generan código asumiendo que todos los hilos se ejecutan en el mismo dominio interno compartible. De hecho, el manual ARMv7 ISA dice

Esta arquitectura (ARMv7) está escrita con la expectativa de que todos los procesadores que usan el mismo sistema operativo o hipervisor estén en el mismo dominio de compartición compartible interno

Por lo tanto, la memoria compartida no coherente entre dominios separados es solo una cosa para el uso explícito específico del sistema de regiones de memoria compartida para la comunicación entre diferentes procesos bajo diferentes núcleos.

Vea también esta discusión de CoreCLR sobre code-gen usando dmb ish(Barrera interna compartible) vs. dmb sy(Sistema) barreras de memoria en ese compilador.

Afirmo que ninguna implementación de C ++ para otro ISA se ejecuta std::threaden núcleos con cachés no coherentes. No tengo pruebas de que no exista tal implementación, pero parece muy poco probable. A menos que esté apuntando a una pieza exótica específica de HW que funcione de esa manera, su pensamiento sobre el rendimiento debe asumir una coherencia de caché similar a MESI entre todos los hilos. ( atomic<T>¡Sin embargo, use preferiblemente de manera que garantice la corrección!)


Los cachés coherentes lo hacen simple

Pero en un sistema multinúcleo con cachés coherentes, implementar un almacén de lanzamiento solo significa ordenar el compromiso en caché para las tiendas de este hilo, sin hacer ningún vaciado explícito. ( https://preshing.com/20120913/acquire-and-release-semantics/ y https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/ ). (Y una carga de adquisición significa ordenar el acceso a la memoria caché en el otro núcleo).

Una instrucción de barrera de memoria simplemente bloquea las cargas y / o almacenes del hilo actual hasta que el búfer de almacenamiento se agota; eso siempre sucede lo más rápido posible por sí solo. ( ¿Una barrera de memoria garantiza que se haya completado la coherencia de la memoria caché? Aborda esta idea errónea). Por lo tanto, si no necesita realizar un pedido, solo tiene que ver la visibilidad en otros hilos, mo_relaxedestá bien. (Y así es volatile, pero no hagas eso).

Consulte también asignaciones de C / C ++ 11 a procesadores

Dato curioso: en x86, cada tienda asm es una tienda de lanzamiento porque el modelo de memoria x86 es básicamente seq-cst más un búfer de tienda (con reenvío de tienda).


Semi-relacionado re: almacenar búfer, visibilidad global y coherencia: C ++ 11 garantiza muy poco. La mayoría de los ISA reales (excepto PowerPC) garantizan que todos los hilos puedan estar de acuerdo en el orden de aparición de dos tiendas por otros dos hilos. (En la terminología formal del modelo de memoria de arquitectura de computadora, son "atómicas multicopia").

Otra idea errónea es que se necesitan instrucciones valla de memoria asm para vaciar el búfer tienda para otros núcleos para ver nuestras tiendas en absoluto . En realidad, el búfer de la tienda siempre intenta agotarse (comprometerse con la caché L1d) lo más rápido posible, de lo contrario se llenaría y detendría la ejecución. Lo que hace una barrera / cerca completa es detener el subproceso actual hasta que el búfer de la tienda se drene , para que nuestras cargas posteriores aparezcan en el orden global después de nuestras tiendas anteriores.

(El modelo de memoria asm fuertemente ordenado volatilede x86 significa que en x86 puede terminar acercándote a él mo_acq_rel, excepto que aún puede ocurrir un reordenamiento en tiempo de compilación con variables no atómicas. Pero la mayoría de los que no son x86 tienen modelos de memoria débilmente ordenados volatiley relaxedson casi iguales a débil como lo mo_relaxedpermite.)

Peter Cordes
fuente
Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
Samuel Liew
2
Gran redacción. Esto es exactamente lo que estaba buscando (dando todos los hechos) en lugar de una declaración general que solo dice "usar atómico en lugar de volátil para una sola bandera booleana global compartida".
bernie
2
@bernie: escribí esto después de frustrarme por las repetidas afirmaciones de que no usar atomicpodría llevar a diferentes hilos que tengan diferentes valores para la misma variable en la memoria caché . / facepalm. En caché, no, en CPU registra sí (con variables no atómicas); Las CPU usan caché coherente. Desearía que otras preguntas sobre SO no estuvieran llenas de explicaciones atomicsobre los conceptos erróneos sobre cómo funcionan las CPU. (Porque eso es algo útil de entender por razones de rendimiento, y también ayuda a explicar por qué las reglas atómicas de ISO C ++ están escritas tal como están.)
Peter Cordes
-1
#include <iostream>
#include <thread>
#include <unistd.h>
using namespace std;

bool checkValue = false;

int main()
{
    std::thread writer([&](){
            sleep(2);
            checkValue = true;
            std::cout << "Value of checkValue set to " << checkValue << std::endl;
        });

    std::thread reader([&](){
            while(!checkValue);
        });

    writer.join();
    reader.join();
}

Una vez, un entrevistador que también creía que lo volátil es inútil argumentó conmigo que la optimización no causaría ningún problema y se refería a diferentes núcleos que tenían líneas de caché separadas y todo eso (realmente no entendía a qué se refería exactamente). Pero este fragmento de código cuando se compila con -O3 en g ++ (g ++ -O3 thread.cpp -lpthread), muestra un comportamiento indefinido. Básicamente, si el valor se establece antes de la comprobación while, funciona bien y, si no, entra en un bucle sin molestarse en buscar el valor (que en realidad fue cambiado por el otro hilo). Básicamente, creo que el valor de checkValue solo se obtiene una vez en el registro y nunca se vuelve a verificar bajo el nivel más alto de optimización. Si se establece en verdadero antes de la recuperación, funciona bien y, si no, entra en un bucle. Por favor corrígeme si estoy equivocado.

Anu Siril
fuente
44
¿Con qué tiene que ver esto volatile? Sí, este código es UB, pero también es UB volatile.
David Schwartz
-2

Necesita volátil y posiblemente bloqueo.

volatile le dice al optimizador que el valor puede cambiar asincrónicamente, por lo tanto

volatile bool flag = false;

while (!flag) {
    /*do something*/
}

leerá la bandera cada vez alrededor del ciclo.

Si desactiva la optimización o hace que cada variable sea volátil, un programa se comportará igual pero más lento. volátil solo significa 'Sé que es posible que lo hayas leído y sé lo que dice, pero si digo leerlo, léelo.

El bloqueo es parte del programa. Entonces, por cierto, si está implementando semáforos, entre otras cosas, deben ser volátiles. (No lo intentes, es difícil, probablemente necesitará un pequeño ensamblador o el nuevo material atómico, y ya está hecho).

ctrl-alt-delor
fuente
1
¿Pero no es este, y el mismo ejemplo en la otra respuesta, ocupado esperando y, por lo tanto, algo que debe evitarse? Si este es un ejemplo artificial, ¿hay ejemplos de la vida real que no sean artificiales?
David Preston
77
@ Chris: La espera ocupada es ocasionalmente una buena solución. En particular, si espera tener que esperar solo un par de ciclos de reloj, conlleva mucho menos sobrecarga que el enfoque mucho más pesado de suspender el hilo. Por supuesto, como he mencionado en otros comentarios, ejemplos como este son defectuosos porque suponen que las lecturas / escrituras en la bandera no se reordenarán con respecto al código que protege, y no se ofrece tal garantía, por lo que , volatileno es realmente útil incluso en este caso. Pero la espera ocupada es una técnica ocasionalmente útil.
jalf
3
@richard Sí y no. La primera mitad es correcta. Pero esto solo significa que la CPU y el compilador no pueden reordenar variables volátiles entre sí. Si leo una variable volátil A, y luego leo una variable volátil B, entonces el compilador debe emitir un código que esté garantizado (incluso con el reordenamiento de la CPU) para leer A antes de B. Pero no ofrece garantías sobre todos los accesos de variables no volátiles . Se pueden reordenar alrededor de su lectura / escritura volátil muy bien. Así que a menos que haga cada variable en su programa volátil, no le dará la garantía de que estés interesado en
JALF
2
@ ctrl-alt-delor: Eso no es lo que volatilesignifica "no reordenar". Espera que signifique que las tiendas serán visibles globalmente (para otros hilos) en el orden del programa. Eso es lo que atomic<T>con memory_order_releaseo seq_cstle da. Pero volatile solo le ofrece una garantía de no reordenar en tiempo de compilación : cada acceso aparecerá en el asm en el orden del programa. Útil para un controlador de dispositivo. Y útil para la interacción con un controlador de interrupciones, depurador o controlador de señales en el núcleo / hilo actual, pero no para interactuar con otros núcleos.
Peter Cordes
1
volatileen la práctica es suficiente para verificar un keep_runningindicador como lo está haciendo aquí: las CPU reales siempre tienen cachés coherentes que no requieren lavado manual. Pero no hay razón para recomendar volatilemás atomic<T>con mo_relaxed; Obtendrás lo mismo.
Peter Cordes