c++ - ¿Qué significa cada memory_order?
c++11 thread-safety (2)
Leí un capítulo y no me gustó mucho. Todavía no estoy claro cuáles son las diferencias entre cada orden de memoria. Esta es mi especulación actual que entendí después de leer el http://en.cppreference.com/w/cpp/atomic/memory_order mucho más simple
Lo siguiente es incorrecto, así que no intentes aprender de él
- memory_order_relaxed: no se sincroniza, pero no se ignora cuando se realiza el pedido desde otro modo en una var atómica diferente
- memory_order_consume: Sincroniza la lectura de esta variable atómica. Sin embargo, no sincroniza vars relajados escritos antes de esto. Sin embargo, si el subproceso usa var X al modificar Y (y lo libera). Otros subprocesos que consuman Y verán también a X lanzado? No sé si esto significa que este hilo empuja los cambios de x (y obviamente y)
- memory_order_acquire: Syncs lee esta variable atómica Y se asegura de que los vars relajados escritos antes de esto también estén sincronizados. (¿significa esto que todas las variables atómicas en todos los hilos están sincronizadas?)
- memory_order_release: empuja el almacén atómico a otros hilos (pero solo si leen var con consumir / adquirir)
- memory_order_acq_rel: para operaciones de lectura / escritura. Hace una adquisición para que no modifique un valor anterior y libere los cambios.
- memory_order_seq_cst: Lo mismo que adquirir release, excepto que obliga a las actualizaciones a verse en otros hilos (si
a
tienda está relajada en otro hilo. I almacenab
con seq_cst. Un tercer hilo leyendoa
con relax verá los cambios junto conb
y cualquier otra variable atómica?).
Creo que entendí, pero corrígeme si estoy equivocado. No pude encontrar nada que lo explicase en inglés fácil de leer.
El Wiki de GCC ofrece una explicación muy completa y fácil de entender con ejemplos de código.
(fragmento editado y énfasis agregado)
IMPORTANTE:
Al volver a leer la cita siguiente copiada de la Wiki de GCC en el proceso de agregar mi propia redacción a la respuesta, noté que la cita es realmente incorrecta. Adquirieron y consumen exactamente de la manera incorrecta. Una operación de liberación de consumo solo proporciona una garantía de pedido en datos dependientes, mientras que una operación de liberación de lanzamiento proporciona esa garantía independientemente de si los datos dependen del valor atómico o no.
El primer modelo es "secuencialmente consistente". Este es el modo predeterminado que se usa cuando no se especifica ninguno, y es el más restrictivo. También se puede especificar explícitamente mediante
memory_order_seq_cst
. Proporciona las mismas restricciones y limitaciones para mover cargas alrededor de las que los programadores secuenciales están familiarizados intrínsecamente, excepto que se aplica a través de subprocesos .
[...]
Desde un punto de vista práctico, esto equivale a todas las operaciones atómicas que actúan como barreras de optimización. Está bien volver a pedir cosas entre operaciones atómicas, pero no a través de la operación. Subprocesos local no se ve afectado ya que no hay visibilidad para otros hilos. [...] Este modo también proporciona consistencia en todos los hilos.El enfoque opuesto es
memory_order_relaxed
. Este modelo permite mucha menos sincronización eliminando las restricciones de pasar antes. Estos tipos de operaciones atómicas también pueden tener varias optimizaciones realizadas en ellas, como la eliminación y el uso compartido de la tienda muerta. [...] Sin que pase nada, antes de los bordes, ningún hilo puede contar con un pedido específico de otro hilo.
El modo relajado se usa más comúnmente cuando el programador simplemente quiere que una variable sea de naturaleza atómica en lugar de usarla para sincronizar hilos para otros datos de memoria compartida.El tercer modo (
memory_order_acquire
/memory_order_release
) es un híbrido entre los otros dos. El modo de adquisición / liberación es similar al modo secuencialmente consistente, excepto que solo aplica una relación de pasar antes a las variables dependientes . Esto permite una relajación de la sincronización requerida entre lecturas independientes de escrituras independientes.
memory_order_consume
es un refinamiento más sutil en el modelo de liberación / adquisición de memoria que relaja los requisitos ligeramente eliminando lo que sucede antes de ordenar también en variables compartidas no dependientes .
[...]
La diferencia real se reduce a la cantidad de estado que el hardware tiene que enjuagar para sincronizar. Como una operación de consumo puede, por lo tanto, ejecutarse más rápido, alguien que sabe lo que está haciendo puede usarla para aplicaciones de rendimiento crítico.
Aquí sigue mi propio intento de una explicación más mundana:
Un enfoque diferente para mirarlo es observar el problema desde el punto de vista de reordenar las lecturas y escrituras, tanto atómicas como ordinarias:
Todas las operaciones atómicas tienen la garantía de ser atómicas dentro de sí mismas (¡la combinación de dos operaciones atómicas no es atómica en su conjunto!) Y de ser visibles en el orden total en el que aparecen en la línea de tiempo del flujo de ejecución. Eso significa que ninguna operación atómica puede, bajo ninguna circunstancia, ser reordenada, pero otras operaciones de memoria podrían serlo. Los compiladores (y las CPU) rutinariamente hacen tal reordenamiento como una optimización.
También significa que el compilador debe usar las instrucciones necesarias para garantizar que una operación atómica que se ejecute en cualquier momento vea los resultados de todas y cada una de las demás operaciones atómicas, posiblemente en otro núcleo del procesador (pero no necesariamente en otras operaciones) que se ejecutaron antes .
Ahora, un relajado es solo eso, lo mínimo. No hace nada además y no ofrece otras garantías. Es la operación más económica posible. Para las operaciones que no son de lectura, modificación y escritura en arquitecturas de procesador fuertemente ordenadas (por ejemplo, x86 / amd64), esto se reduce a un movimiento ordinario normal normal.
La operación consecuentemente secuencial es exactamente lo contrario, impone un ordenamiento estricto no solo para operaciones atómicas, sino también para otras operaciones de memoria que ocurren antes o después. Ninguno puede cruzar la barrera impuesta por la operación atómica. Prácticamente, esto significa oportunidades de optimización perdidas, y posiblemente se tengan que insertar instrucciones de cercas. Este es el modelo más caro.
Una operación de liberación impide que se reordenen las cargas y almacenes ordinarios después de la operación atómica, mientras que una operación de adquisición impide que las cargas y almacenes ordinarios se reordenen antes de la operación atómica. Todo lo demás puede moverse.
La combinación de evitar que las tiendas se muevan después y las cargas que se mueven antes de la operación atómica respectiva garantiza que todo lo que el hilo adquirente vea es coherente, con solo una pequeña cantidad de oportunidad de optimización perdida.
Uno puede pensar en eso como algo así como una cerradura inexistente que está siendo liberada (por el escritor) y adquirida (por el lector). Excepto ... no hay cerradura.
En la práctica, liberar / adquirir usualmente significa que el compilador no necesita usar ninguna instrucción especial particularmente costosa, pero no puede reordenar libremente las cargas y las tiendas a su gusto, lo que puede perder algunas oportunidades (pequeñas) de optimización.
Finalmente, consumir es la misma operación que adquirir , solo con la excepción de que las garantías de pedido solo se aplican a los datos dependientes. Los datos dependientes serían, por ejemplo, datos apuntados por un puntero modificado atómicamente.
Podría decirse que eso puede proporcionar un par de oportunidades de optimización que no están presentes en las operaciones de adquisición (ya que hay menos datos sujetos a restricciones), sin embargo, esto ocurre a expensas de un código más complejo y propenso a errores, y la tarea no trivial de obtener cadenas de dependencia correctas.
Actualmente se desaconseja usar el orden de consumo mientras se está revisando la especificación.
Este es un tema bastante complejo. Intente leer http://en.cppreference.com/w/cpp/atomic/memory_order varias veces, intente leer otros recursos, etc.
Aquí hay una descripción simplificada:
El compilador y la CPU pueden reordenar los accesos a la memoria. Es decir, pueden suceder en un orden diferente al especificado en el código. Eso está bien la mayor parte del tiempo, el problema surge cuando diferentes hilos intentan comunicarse y pueden ver tal orden de acceso a la memoria que rompe las invariantes del código.
Por lo general, puede usar bloqueos para la sincronización. El problema es que son lentos. Las operaciones atómicas son mucho más rápidas, porque la sincronización ocurre a nivel de CPU (es decir, la CPU garantiza que ningún otro hilo, incluso en otra CPU, modifique alguna variable, etc.).
Entonces, el único problema al que nos enfrentamos es el reordenamiento de los accesos a la memoria. memory_order
enum especifica qué tipos de reordenamientos debe prohibir el compilador.
relaxed
- sin restricciones.
consume
: ninguna carga que dependa del valor recién cargado se puede reordenar wrt. la carga atómica Es decir, si buscan la carga atómica en el código fuente, también ocurrirán después de la carga atómica.
acquire
- no se pueden reordenar las cargas wrt. la carga atómica Es decir, si buscan la carga atómica en el código fuente, también ocurrirán después de la carga atómica.
release
- no se pueden reordenar las tiendas wrt. la tienda atómica Es decir, si están antes de la tienda atómica en el código fuente, también pasarán antes que la tienda atómica.
acq_rel
: acquire
y release
combinados.
seq_cst
: es más difícil entender por qué se requiere este orden. Básicamente, todos los demás pedidos solo aseguran que los reordenamientos no permitidos específicos no sucedan solo para los hilos que consumen / liberan la misma variable atómica. Los accesos a la memoria aún se pueden propagar a otros hilos en cualquier orden. Este orden asegura que esto no ocurra (por lo tanto, consistencia secuencial). Para un caso donde esto es necesario, vea el ejemplo al final de la página vinculada.