Namespaces
Variants

memory_order

From cppreference.net
Definido en el encabezado <stdatomic.h>
enum memory_order

{
memory_order_relaxed,
memory_order_consume,
memory_order_acquire,
memory_order_release,
memory_order_acq_rel,
memory_order_seq_cst

} ;
(desde C11)

memory_order especifica cómo se ordenan los accesos a memoria, incluyendo los accesos regulares no atómicos, alrededor de una operación atómica. Sin restricciones en un sistema multinúcleo, cuando múltiples hilos leen y escriben simultáneamente en varias variables, un hilo puede observar que los valores cambian en un orden diferente al orden en que otro hilo los escribió. De hecho, el orden aparente de los cambios puede incluso diferir entre múltiples hilos lectores. Algunos efectos similares pueden ocurrir incluso en sistemas monoprocesador debido a las transformaciones del compilador permitidas por el modelo de memoria.

El comportamiento predeterminado de todas las operaciones atómicas en el lenguaje y la biblioteca proporciona un ordenamiento secuencialmente consistente (ver discusión abajo). Ese valor predeterminado puede afectar el rendimiento, pero a las operaciones atómicas de la biblioteca se les puede proporcionar un argumento adicional memory_order para especificar las restricciones exactas, más allá de la atomicidad, que el compilador y el procesador deben hacer cumplir para esa operación.

Contenidos

Constantes

Definido en el encabezado <stdatomic.h>
Valor Explicación
memory_order_relaxed Operación relajada: no se imponen restricciones de sincronización u ordenamiento sobre otras lecturas o escrituras, solo se garantiza la atomicidad de esta operación (ver Ordenamiento relajado abajo).
memory_order_consume
(obsoleto en C++26)
Una operación de carga con este orden de memoria realiza una operación de consumo en la ubicación de memoria afectada: ninguna lectura o escritura en el hilo actual que dependa del valor cargado actualmente puede reordenarse antes de esta carga. Las escrituras a variables dependientes de datos en otros hilos que liberan la misma variable atómica son visibles en el hilo actual. En la mayoría de plataformas, esto solo afecta a las optimizaciones del compilador (ver Ordenamiento liberación-consumo abajo).
memory_order_acquire Una operación de carga con este orden de memoria realiza la operación de adquisición en la ubicación de memoria afectada: ninguna lectura o escritura en el hilo actual puede reordenarse antes de esta carga. Todas las escrituras en otros hilos que liberan la misma variable atómica son visibles en el hilo actual (ver Ordenamiento liberación-adquisición abajo).
memory_order_release Una operación de almacenamiento con este orden de memoria realiza la operación de liberación : ninguna lectura o escritura en el hilo actual puede reordenarse después de este almacenamiento. Todas las escrituras en el hilo actual son visibles en otros hilos que adquieren la misma variable atómica (ver Ordenamiento liberación-adquisición abajo) y las escrituras que llevan una dependencia a la variable atómica se vuelven visibles en otros hilos que consumen la misma variable atómica (ver Ordenamiento liberación-consumo abajo).
memory_order_acq_rel Una operación de lectura-modificación-escritura con este orden de memoria es tanto una operación de adquisición como una operación de liberación . Ninguna lectura o escritura de memoria en el hilo actual puede reordenarse antes de la carga, ni después del almacenamiento. Todas las escrituras en otros hilos que liberan la misma variable atómica son visibles antes de la modificación y la modificación es visible en otros hilos que adquieren la misma variable atómica.
memory_order_seq_cst Una operación de carga con este orden de memoria realiza una operación de adquisición , un almacenamiento realiza una operación de liberación , y lectura-modificación-escritura realiza tanto una operación de adquisición como una operación de liberación , además existe un único orden total en el cual todos los hilos observan todas las modificaciones en el mismo orden (ver Ordenamiento secuencialmente consistente abajo).

Ordenamiento relajado

Las operaciones atómicas etiquetadas con memory_order_relaxed no son operaciones de sincronización; no imponen un orden entre los accesos concurrentes a memoria. Solo garantizan atomicidad y consistencia del orden de modificación.

Por ejemplo, con x y y inicialmente en cero,

// Hilo 1:
r1 = atomic_load_explicit ( y, memory_order_relaxed ) ; // A
atomic_store_explicit ( x, r1, memory_order_relaxed ) ; // B
// Hilo 2:
r2 = atomic_load_explicit ( x, memory_order_relaxed ) ; // C
atomic_store_explicit ( y, 42 , memory_order_relaxed ) ; // D
puede producir r1 == r2 == 42 porque, aunque A está secuenciado-antes de B dentro del hilo 1 y C está secuenciado antes de D dentro del hilo 2, nada impide que D aparezca antes que A en el orden de modificación de y , y que B aparezca antes que C en el orden de modificación de x . El efecto secundario de D sobre y podría ser visible para la carga A en el hilo 1 mientras que el efecto secundario de B sobre x podría ser visible para la carga C en el hilo 2. En particular, esto puede ocurrir si D se completa antes que C en el hilo 2, ya sea debido a reordenamiento del compilador o en tiempo de ejecución.

El uso típico para el orden de memoria relajado es incrementar contadores, como los contadores de referencia, ya que esto solo requiere atomicidad, pero no ordenamiento o sincronización.

Ordenamiento Liberar-Consumir

Si un almacenamiento atómico en el hilo A está etiquetado como memory_order_release , una carga atómica en el hilo B de la misma variable está etiquetada como memory_order_consume , y la carga en el hilo B lee un valor escrito por el almacenamiento en el hilo A, entonces el almacenamiento en el hilo A está ordenado por dependencia antes de la carga en el hilo B.

Todas las escrituras de memoria (no atómicas y atómicas relajadas) que happened-before el almacenamiento atómico desde el punto de vista del hilo A, se convierten en visible side-effects dentro de aquellas operaciones en el hilo B en las que la operación de carga carries dependency , es decir, una vez que se completa la carga atómica, aquellos operadores y funciones en el hilo B que utilizan el valor obtenido de la carga tienen garantizado ver lo que el hilo A escribió en la memoria.

La sincronización se establece únicamente entre los hilos que liberan y que consumen la misma variable atómica. Otros hilos pueden ver un orden diferente de accesos a memoria que cualquiera o ambos de los hilos sincronizados.

En todas las CPU principales excepto DEC Alpha, el ordenamiento de dependencias es automático, no se emiten instrucciones adicionales de CPU para este modo de sincronización, solo ciertas optimizaciones del compilador se ven afectadas (por ejemplo, se prohíbe al compilador realizar cargas especulativas en los objetos que forman parte de la cadena de dependencias).

Los casos de uso típicos para este ordenamiento implican acceso de lectura a estructuras de datos concurrentes raramente escritas (tablas de enrutamiento, configuración, políticas de seguridad, reglas de firewall, etc.) y situaciones de publicador-suscriptor con publicación mediada por punteros, es decir, cuando el productor publica un puntero a través del cual el consumidor puede acceder a la información: no es necesario hacer visible todo lo demás que el productor escribió en la memoria al consumidor (lo cual puede ser una operación costosa en arquitecturas débilmente ordenadas). Un ejemplo de tal escenario es rcu_dereference .

Tenga en cuenta que actualmente (2/2015) ningún compilador de producción conocido rastrea cadenas de dependencia: las operaciones consume se elevan a operaciones acquire.

Secuencia de liberación

Si algún atómico es almacenado con liberación y varios otros hilos realizan operaciones de lectura-modificación-escritura sobre ese atómico, se forma una "secuencia de liberación": todos los hilos que realizan las operaciones de lectura-modificación-escritura sobre el mismo atómico se sincronizan con el primer hilo y entre sí, incluso si no tienen la semántica de memory_order_release . Esto hace posibles las situaciones de productor único - múltiples consumidores sin imponer una sincronización innecesaria entre los hilos consumidores individuales.

Ordenamiento Liberar-Adquirir

Si un almacenamiento atómico en el hilo A está etiquetado como memory_order_release , una carga atómica en el hilo B de la misma variable está etiquetada como memory_order_acquire , y la carga en el hilo B lee un valor escrito por el almacenamiento en el hilo A, entonces el almacenamiento en el hilo A sincroniza-con la carga en el hilo B.

Todas las escrituras de memoria (incluyendo no atómicas y atómicas relajadas) que happened-before del almacenamiento atómico desde el punto de vista del hilo A, se convierten en visible side-effects en el hilo B. Es decir, una vez que se completa la carga atómica, se garantiza que el hilo B verá todo lo que el hilo A escribió en la memoria. Esta promesa solo se mantiene si B realmente devuelve el valor que A almacenó, o un valor posterior en la secuencia de liberación.

La sincronización se establece únicamente entre los hilos que liberan y que adquieren la misma variable atómica. Otros hilos pueden ver un orden diferente de accesos a memoria que cualquiera o ambos de los hilos sincronizados.

En sistemas fuertemente ordenados — x86, SPARC TSO, mainframe de IBM, etc. — el ordenamiento release-acquire es automático para la mayoría de operaciones. No se emiten instrucciones adicionales de CPU para este modo de sincronización; solo ciertas optimizaciones del compilador se ven afectadas (por ejemplo, se prohíbe al compilador mover almacenamientos no atómicos más allá del almacenamiento atómico-release o realizar cargas no atómicas antes que la carga atómica-acquire). En sistemas débilmente ordenados (ARM, Itanium, PowerPC), se utilizan instrucciones especiales de carga de CPU o barreras de memoria.

Los bloqueos de exclusión mutua, como mutexes o atomic spinlocks , son un ejemplo de sincronización de liberación-adquisición: cuando el bloqueo es liberado por el hilo A y adquirido por el hilo B, todo lo que ocurrió en la sección crítica (antes de la liberación) en el contexto del hilo A debe ser visible para el hilo B (después de la adquisición) que está ejecutando la misma sección crítica.

Ordenación secuencialmente consistente

Las operaciones atómicas etiquetadas con memory_order_seq_cst no solo ordenan la memoria de la misma manera que el ordenamiento release/acquire (todo lo que happened-before un almacenamiento en un hilo se convierte en un efecto secundario visible en el hilo que realizó una carga), sino que también establecen un orden de modificación total único de todas las operaciones atómicas que están así etiquetadas.

Formalmente,

cada memory_order_seq_cst operación B que carga desde la variable atómica M, observa una de las siguientes:

  • el resultado de la última operación A que modificó M, que aparece antes de B en el orden total único,
  • O, si existió tal A, B puede observar el resultado de alguna modificación en M que no es memory_order_seq_cst y no happen-before A,
  • O, si no existió tal A, B puede observar el resultado de alguna modificación no relacionada de M que no es memory_order_seq_cst .

Si había una memory_order_seq_cst atomic_thread_fence operación X sequenced-before B, entonces B observa una de las siguientes:

  • la última modificación memory_order_seq_cst de M que aparece antes de X en el orden total único,
  • alguna modificación no relacionada de M que aparece más tarde en el orden de modificación de M.

Para un par de operaciones atómicas sobre M llamadas A y B, donde A escribe y B lee el valor de M, si hay dos memory_order_seq_cst atomic_thread_fence s X e Y, y si A está secuenciada-antes de X, Y está secuenciada-antes de B, y X aparece antes que Y en el Orden Total Único, entonces B observa ya sea:

  • el efecto de A,
  • alguna modificación no relacionada de M que aparece después de A en el orden de modificación de M.

Para un par de modificaciones atómicas de M llamadas A y B, B ocurre después de A en el orden de modificación de M si

  • existe un memory_order_seq_cst atomic_thread_fence X tal que A está secuenciado-antes de X y X aparece antes que B en el Orden Total Único,
  • o, existe un memory_order_seq_cst atomic_thread_fence Y tal que Y está secuenciado-antes de B y A aparece antes que Y en el Orden Total Único,
  • o, existen memory_order_seq_cst atomic_thread_fence s X e Y tal que A está secuenciado-antes de X, Y está secuenciado-antes de B, y X aparece antes que Y en el Orden Total Único.

Tenga en cuenta que esto significa que:

1) tan pronto como las operaciones atómicas que no están etiquetadas como memory_order_seq_cst entran en escena, se pierde la consistencia secuencial,
2) las barreras de consistencia secuencial solo establecen un orden total para las propias barreras, no para las operaciones atómicas en el caso general ( sequenced-before no es una relación entre hilos, a diferencia de happens-before ).

La ordenación secuencial puede ser necesaria en situaciones de múltiples productores-múltiples consumidores donde todos los consumidores deben observar las acciones de todos los productores ocurriendo en el mismo orden.

La ordenación secuencial total requiere una instrucción de barrera de memoria completa de la CPU en todos los sistemas multinúcleo. Esto puede convertirse en un cuello de botella de rendimiento ya que fuerza a que los accesos a memoria afectados se propaguen a cada núcleo.

Relación con volatile

Dentro de un hilo de ejecución, los accesos (lecturas y escrituras) a través de volatile lvalues no pueden ser reordenados más allá de efectos secundarios observables (incluyendo otros accesos volátiles) que están separados por un punto de secuencia dentro del mismo hilo, pero este orden no está garantizado que sea observado por otro hilo, ya que el acceso volátil no establece sincronización entre hilos.

Además, los accesos volátiles no son atómicos (la lectura y escritura concurrente es una data race ) y no ordenan la memoria (los accesos a memoria no volátiles pueden reordenarse libremente alrededor del acceso volátil).

Una excepción notable es Visual Studio, donde, con la configuración predeterminada, cada escritura volatile tiene semántica de liberación y cada lectura volatile tiene semántica de adquisición ( Microsoft Docs ), y por lo tanto los volatiles pueden usarse para sincronización entre hilos. La semántica estándar de volatile no es aplicable a la programación multiproceso, aunque son suficientes para, por ejemplo, comunicación con un manejador de señales que se ejecuta en el mismo hilo cuando se aplica a variables sig_atomic_t . La opción del compilador /volatile:iso puede usarse para restaurar el comportamiento consistente con el estándar, que es la configuración predeterminada cuando la plataforma objetivo es ARM.

Ejemplos

Referencias

  • Estándar C23 (ISO/IEC 9899:2024):
  • 7.17.1/4 memory_order (p: TBD)
  • 7.17.3 Orden y consistencia (p: TBD)
  • Estándar C17 (ISO/IEC 9899:2018):
  • 7.17.1/4 memory_order (p: 200)
  • 7.17.3 Orden y consistencia (p: 201-203)
  • Estándar C11 (ISO/IEC 9899:2011):
  • 7.17.1/4 memory_order (p: 273)
  • 7.17.3 Orden y consistencia (p: 275-277)

Véase también

Documentación de C++ para memory order

Enlaces externos

1. Protocolo MOESI
2. x86-TSO: Un Modelo de Programador Riguroso y Utilizable para Multiprocesadores x86 P. Sewell et. al., 2010
3. Una Introducción Tutorial a los Modelos de Memoria Relajada ARM y POWER P. Sewell et al, 2012
4. MESIF: Un Protocolo de Coherencia de Caché de Dos Saltos para Interconexiones Punto a Punto J.R. Goodman, H.H.J. Hum, 2009
5. Modelos de Memoria Russ Cox, 2021