Namespaces
Variants

std:: memory_order

From cppreference.net
Concurrency support library
Threads
(C++11)
(C++20)
this_thread namespace
(C++11)
(C++11)
Cooperative cancellation
Mutual exclusion
Generic lock management
Condition variables
(C++11)
Semaphores
Latches and Barriers
(C++20)
(C++20)
Futures
(C++11)
(C++11)
(C++11)
Safe reclamation
Hazard pointers
Atomic types
(C++11)
(C++20)
Initialization of atomic types
(C++11) (deprecated in C++20)
(C++11) (deprecated in C++20)
Memory ordering
memory_order
(C++11)
(C++11) (deprecated in C++26)
Free functions for atomic operations
Free functions for atomic flags
Definido en el encabezado <atomic>
enum memory_order

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

} ;
(desde C++11)
(hasta C++20)
enum class memory_order : /* no especificado */

{
relaxed, consume, acquire, release, acq_rel, seq_cst
} ;
inline constexpr memory_order memory_order_relaxed = memory_order :: relaxed ;
inline constexpr memory_order memory_order_consume = memory_order :: consume ;
inline constexpr memory_order memory_order_acquire = memory_order :: acquire ;
inline constexpr memory_order memory_order_release = memory_order :: release ;
inline constexpr memory_order memory_order_acq_rel = memory_order :: acq_rel ;

inline constexpr memory_order memory_order_seq_cst = memory_order :: seq_cst ;
(desde C++20)

std::memory_order especifica cómo se deben ordenar los accesos a memoria, incluidos 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 transformaciones del compilador permitidas por el modelo de memoria.

El comportamiento predeterminado de todas las operaciones atómicas en 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 dar un argumento adicional std::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 <atomic>
Nombre Significado
memory_order_relaxed Operación relajada: no se imponen restricciones de sincronización u ordenamiento en 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 dependiente 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 afecta solo 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).

Descripción formal

La sincronización entre hilos y el ordenamiento de memoria determinan cómo se ordenan las evaluaciones y los efectos secundarios de las expresiones entre diferentes hilos de ejecución. Se definen en los siguientes términos:

Secuenciado-antes

Dentro del mismo hilo, la evaluación A puede estar sequenced-before la evaluación B, como se describe en evaluation order .

Transmisión de dependencia

Dentro del mismo hilo, la evaluación A que está secuenciada-antes de la evaluación B también puede transmitir una dependencia a B (es decir, B depende de A), si se cumple alguna de las siguientes condiciones:

1) El valor de A se utiliza como operando de B, excepto
a) si B es una llamada a std::kill_dependency ,
b) si A es el operando izquierdo de los operadores incorporados && , || , ?: , o , .
2) A escribe en un objeto escalar M, B lee de M.
3) A transmite dependencia a otra evaluación X, y X transmite dependencia a B.
(hasta C++26)

Orden de modificación

Todas las modificaciones a cualquier variable atómica particular ocurren en un orden total que es específico para esta única variable atómica.

Las siguientes cuatro garantías se aplican a todas las operaciones atómicas:

1) Coherencia de escritura-escritura : Si la evaluación A que modifica algún atómico M (una escritura) sucede-antes de la evaluación B que modifica M, entonces A aparece antes que B en el orden de modificación de M.
2) Coherencia lectura-lectura : si una computación de valor A de algún atómico M (una lectura) sucede-antes que una computación de valor B sobre M, y si el valor de A proviene de una escritura X sobre M, entonces el valor de B es o bien el valor almacenado por X, o el valor almacenado por un efecto secundario Y sobre M que aparece después de X en el orden de modificación de M.
3) Coherencia de lectura-escritura : si una computación de valor A de algún atómico M (una lectura) sucede-antes de una operación B sobre M (una escritura), entonces el valor de A proviene de un efecto secundario (una escritura) X que aparece antes que B en el orden de modificación de M.
4) Coherencia de escritura-lectura : si un efecto secundario (una escritura) X en un objeto atómico M happens-before un cálculo de valor (una lectura) B de M, entonces la evaluación B tomará su valor de X o de un efecto secundario Y que sigue a X en el orden de modificación de M.

Secuencia de liberación

Después de que se realice una operación de liberación A sobre un objeto atómico M, la subsecuencia continua más larga del orden de modificación de M que consiste en:

1) Escrituras realizadas por el mismo hilo que realizó A.
(until C++20)
2) Operaciones atómicas de lectura-modificación-escritura realizadas en M por cualquier hilo.

Se conoce como release sequence headed by A .

Se sincroniza con

Si un almacenamiento atómico en el hilo A es una operación de liberación , una carga atómica en el hilo B de la misma variable es una operación de adquisición , 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 se sincroniza con la carga en el hilo B.

Además, algunas llamadas de biblioteca pueden estar definidas para synchronize-with otras llamadas de biblioteca en otros hilos.

Ordenado por dependencia antes

Entre hilos, la evaluación A está ordenada por dependencia antes de la evaluación B si cualquiera de las siguientes condiciones es verdadera:

1) A realiza una operación de liberación en algún atómico M, y, en un hilo diferente, B realiza una operación de consumo en el mismo atómico M, y B lee un valor escrito por cualquier parte de la secuencia de liberación encabezada (hasta C++20) por A.
2) A está ordenado por dependencia antes de X y X lleva una dependencia a B.
(hasta C++26)

Sucede-antes entre hilos

Entre hilos, la evaluación A inter-thread happens before la evaluación B si cualquiera de las siguientes condiciones es verdadera:

1) A synchronizes-with B.
2) A está dependency-ordered before B.
3) A synchronizes-with alguna evaluación X, y X está sequenced-before B.
4) A está sequenced-before alguna evaluación X, y X inter-thread happens-before B.
5) Una inter-thread happens-before precede a alguna evaluación X, y X inter-thread happens-before precede a B.


Happens-before

Independientemente de los hilos, la evaluación A happens-before la evaluación B si cualquiera de las siguientes es verdadera:

1) A está sequenced-before B.
2) A inter-thread happens before B.

Se requiere que la implementación garantice que la relación happens-before sea acíclica, introduciendo sincronización adicional si es necesario (solo puede ser necesario si está involucrada una operación consume, ver Batty et al ).

Si una evaluación modifica una ubicación de memoria, y la otra lee o modifica la misma ubicación de memoria, y si al menos una de las evaluaciones no es una operación atómica, el comportamiento del programa es indefinido (el programa tiene una data race ) a menos que exista una relación happens-before entre estas dos evaluaciones.

Simply happens-before

Independientemente de los hilos, la evaluación A simply happens-before la evaluación B si cualquiera de las siguientes es verdadera:

1) A está sequenced-before B.
2) A synchronizes-with B.
3) A simply happens-before X, y X simply happens-before B.

Nota: sin operaciones consume, las relaciones simply happens-before y happens-before son iguales.

(since C++20)
(until C++26)

Happens-before

Independientemente de los hilos, la evaluación A happens-before la evaluación B si cualquiera de las siguientes es verdadera:

1) A está sequenced-before B.
2) A synchronizes-with B.
3) A happens-before X, y X happens-before B.
(since C++26)

Sucede fuertemente antes

Independientemente de los hilos, la evaluación A strongly happens-before la evaluación B si cualquiera de las siguientes condiciones es verdadera:

1) A está sequenced-before B.
2) A synchronizes-with B.
3) A strongly happens-before X, y X strongly happens-before B.
(hasta C++20)
1) A está sequenced-before B.
2) A synchronizes with B, y tanto A como B son operaciones atómicas secuencialmente consistentes.
3) A está sequenced-before X, X simplemente (hasta C++26) happens-before Y, e Y está sequenced-before B.
4) A strongly happens-before X, y X strongly happens-before B.

Nota: informalmente, si A strongly happens-before B, entonces A parece evaluarse antes que B en todos los contextos.

Nota: strongly happens-before excluye operaciones consume.

(hasta C++26)
(desde C++20)

Efectos secundarios visibles

El efecto secundario A sobre un escalar M (una escritura) es visible con respecto al cálculo de valor B sobre M (una lectura) si ambas condiciones siguientes son verdaderas:

1) A happens-before B.
2) No existe otro efecto secundario X en M donde A happens-before X y X happens-before B.

Si el efecto secundario A es visible con respecto al cálculo de valor B, entonces el subconjunto contiguo más largo de los efectos secundarios sobre M, en orden de modificación , donde B no sucede-antes de él se conoce como la secuencia visible de efectos secundarios (el valor de M, determinado por B, será el valor almacenado por uno de estos efectos secundarios).

Nota: la sincronización entre hilos se reduce a prevenir carreras de datos (estableciendo relaciones de sucede-antes) y definir qué efectos secundarios se vuelven visibles bajo qué condiciones.

Operación de consumo

La carga atómica con memory_order_consume o más fuerte es una operación de consumo. Nótese que std::atomic_thread_fence impone requisitos de sincronización más fuertes que una operación de consumo.

Operación de adquisición

La carga atómica con memory_order_acquire o más fuerte es una operación de adquisición. La operación lock() en un Mutex también es una operación de adquisición. Nótese que std::atomic_thread_fence impone requisitos de sincronización más fuertes que una operación de adquisición.

Operación de liberación

La operación de almacenamiento atómico con memory_order_release o más fuerte es una operación de liberación. La operación unlock() en un Mutex también es una operación de liberación. Nótese que std::atomic_thread_fence impone requisitos de sincronización más fuertes que una operación de liberación.

Explicación

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 = y.load(std::memory_order_relaxed); // A
x.store(r1, std::memory_order_relaxed); // B
// Hilo 2:
r2 = x.load(std::memory_order_relaxed); // C 
y.store(42, std::memory_order_relaxed); // D

se permite 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 en y podría ser visible para la carga A en el hilo 1 mientras que el efecto secundario de B en 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 durante la ejecución.

Incluso con el modelo de memoria relajado, no se permiten valores "de la nada" que dependan circularmente de sus propios cálculos, por ejemplo, con x e y inicialmente en cero,

// Thread 1:
r1 = y.load(std::memory_order_relaxed);
if (r1 == 42)
    x.store(r1, std::memory_order_relaxed);
// Thread 2:
r2 = x.load(std::memory_order_relaxed);
if (r2 == 42)
    y.store(42, std::memory_order_relaxed);

no se permite producir r1 == r2 == 42 ya que el almacenamiento de 42 en y solo es posible si el almacenamiento en x almacena 42 , lo cual depende circularmente del almacenamiento en y que almacena 42 . Nótese que hasta C++14, esto estaba técnicamente permitido por la especificación, pero no se recomendaba a los implementadores.

(desde C++14)

Un uso típico para el orden de memoria relajado es incrementar contadores, como los contadores de referencia de std::shared_ptr , ya que esto solo requiere atomicidad, pero no ordenamiento o sincronización (nótese que decrementar los std::shared_ptr contadores requiere sincronización de adquisición-liberación con el destructor).

#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
std::atomic<int> cnt = {0};
void f()
{
    for (int n = 0; n < 1000; ++n)
        cnt.fetch_add(1, std::memory_order_relaxed);
}
int main()
{
    std::vector<std::thread> v;
    for (int n = 0; n < 10; ++n)
        v.emplace_back(f);
    for (auto& t : v)
        t.join();
    std::cout << "Final counter value is " << cnt << '\n';
}

Salida:

Final counter value is 10000

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 las 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 cumple 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 std::mutex o atomic spinlock , 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.

#include <atomic>
#include <cassert>
#include <string>
#include <thread>
std::atomic<std::string*> ptr;
int data;
void producer()
{
    std::string* p = new std::string("Hello");
    data = 42;
    ptr.store(p, std::memory_order_release);
}
void consumer()
{
    std::string* p2;
    while (!(p2 = ptr.load(std::memory_order_acquire)))
        ;
    assert(*p2 == "Hello"); // nunca falla
    assert(data == 42); // nunca falla
}
int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join(); t2.join();
}

El siguiente ejemplo demuestra el ordenamiento transitivo liberación-adquisición a través de tres hilos, utilizando una secuencia de liberación.

#include <atomic>
#include <cassert>
#include <thread>
#include <vector>
std::vector<int> data;
std::atomic<int> flag = {0};
void thread_1()
{
    data.push_back(42);
    flag.store(1, std::memory_order_release);
}
void thread_2()
{
    int expected = 1;
    // memory_order_relaxed is okay because this is an RMW,
    // and RMWs (with any ordering) following a release form a release sequence
    while (!flag.compare_exchange_strong(expected, 2, std::memory_order_relaxed))
    {
        expected = 1;
    }
}
void thread_3()
{
    while (flag.load(std::memory_order_acquire) < 2)
        ;
    // if we read the value 2 from the atomic flag, we see 42 in the vector
    assert(data.at(0) == 42); // will never fire
}
int main()
{
    std::thread a(thread_1);
    std::thread b(thread_2);
    std::thread c(thread_3);
    a.join(); b.join(); c.join();
}

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 ocurrieron antes del almacenamiento atómico desde el punto de vista del hilo A, se convierten en efectos secundarios visibles dentro de aquellas operaciones en el hilo B en las que la operación de carga transporta dependencia , es decir, una vez que se completa la carga atómica, se garantiza que aquellos operadores y funciones en el hilo B que utilizan el valor obtenido de la carga verán lo que el hilo A escribió en la memoria.

La sincronización se establece únicamente entre los hilos que liberan y 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 por dependencia es automático; no se emiten instrucciones adicionales de CPU para este modo de sincronización, solo se ven afectadas ciertas optimizaciones del compilador (por ejemplo, se prohíbe al compilador realizar cargas especulativas en los objetos que forman parte de la cadena de dependencia).

Los casos de uso típicos para este ordenamiento implican acceso de lectura a estructuras de datos concurrentes poco modificadas (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 .

Véase también std::kill_dependency y [[ carries_dependency ]] para un control granular de la cadena de dependencias.

Nótese que actualmente (2/2015) ningún compilador de producción conocido rastrea cadenas de dependencia: las operaciones de consumo se elevan a operaciones de adquisición.

(until C++26)

La especificación del ordenamiento release-consume está siendo revisada, y se desaconseja temporalmente el uso de memory_order_consume .

(since C++17)
(until C++26)

El ordenamiento release-consume tiene el mismo efecto que el ordenamiento release-acquire y está obsoleto.

(since C++26)

Este ejemplo demuestra la sincronización ordenada por dependencia para la publicación mediada por puntero: el entero data no está relacionado con el puntero a string por una relación de dependencia de datos, por lo tanto su valor es indefinido en el consumidor.

#include <atomic>
#include <cassert>
#include <string>
#include <thread>
std::atomic<std::string*> ptr;
int data;
void producer()
{
    std::string* p = new std::string("Hello");
    data = 42;
    ptr.store(p, std::memory_order_release);
}
void consumer()
{
    std::string* p2;
    while (!(p2 = ptr.load(std::memory_order_consume)))
        ;
    assert(*p2 == "Hello"); // never fires: *p2 carries dependency from ptr
    assert(data == 42); // may or may not fire: data does not carry dependency from ptr
}
int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join(); t2.join();
}


Ordenamiento secuencialmente consistente

Las operaciones atómicas etiquetadas con memory_order_seq_cst no solo ordenan la memoria de la misma manera que el ordenamiento de liberación/adquisición (todo lo que ocurrió-antes de 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 etiquetadas de esta manera.

Formalmente,

cada operación memory_order_seq_cst 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 existía 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 existía tal A, B puede observar el resultado de alguna modificación no relacionada de M que no es memory_order_seq_cst .

Si existía una operación memory_order_seq_cst std::atomic_thread_fence 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 en M llamadas A y B, donde A escribe y B lee el valor de M, si existen dos memory_order_seq_cst std::atomic_thread_fence s X e Y, y si A está sequenced-before X, Y está sequenced-before B, y X aparece antes de Y en el Orden Total Único, entonces B observa:

  • 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 std::atomic_thread_fence X tal que A está sequenced-before X y X aparece antes de B en el Orden Total Único,
  • o, existe un memory_order_seq_cst std::atomic_thread_fence Y tal que Y está sequenced-before B y A aparece antes de Y en el Orden Total Único,
  • o, existen memory_order_seq_cst std::atomic_thread_fence s X e Y tales que A está sequenced-before X, Y está sequenced-before B, y X aparece antes de Y en el Orden Total Único.

Nótese que esto significa que:

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

una operación atómica A sobre algún objeto atómico M está ordenada-por-coherencia-antes de otra operación atómica B sobre M si se cumple alguna de las siguientes condiciones:

1) A es una modificación, y B lee el valor almacenado por A,
2) A precede a B en el orden de modificación de M,
3) A lee el valor almacenado por una modificación atómica X, X precede a B en el orden de modificación , y A y B no son la misma operación atómica de lectura-modificación-escritura,
4) A está ordenada-por-coherencia-antes de X, y X está ordenada-por-coherencia-antes de B.

Existe un único orden total S en todas las operaciones memory_order_seq_cst , incluyendo barreras, que satisface las siguientes restricciones:

1) si A y B son operaciones memory_order_seq_cst , y A fuertemente precede-en-tiempo a B, entonces A precede a B en S,
2) para cada par de operaciones atómicas A y B sobre un objeto M, donde A está ordenada-por-coherencia-antes de B:
a) si A y B son ambas operaciones memory_order_seq_cst , entonces A precede a B en S,
b) si A es una operación memory_order_seq_cst , y B precede-en-tiempo a una barrera memory_order_seq_cst Y, entonces A precede a Y en S,
c) si una barrera memory_order_seq_cst X precede-en-tiempo a A, y B es una operación memory_order_seq_cst , entonces X precede a B en S,
d) si una barrera memory_order_seq_cst X precede-en-tiempo a A, y B precede-en-tiempo a una barrera memory_order_seq_cst Y, entonces X precede a Y en S.

La definición formal asegura que:

1) el único orden total es consistente con el orden de modificación de cualquier objeto atómico,
2) una carga memory_order_seq_cst obtiene su valor ya sea de la última modificación memory_order_seq_cst , o de alguna modificación no- memory_order_seq_cst que no precede-en-tiempo a modificaciones memory_order_seq_cst anteriores.

El único orden total podría no ser consistente con precede-en-tiempo . Esto permite una implementación más eficiente de memory_order_acquire y memory_order_release en algunas CPUs. Puede producir resultados sorprendentes cuando memory_order_acquire y memory_order_release se mezclan con memory_order_seq_cst .

Por ejemplo, con x e y inicialmente cero,

// Hilo 1:
x.store(1, std::memory_order_seq_cst); // A
y.store(1, std::memory_order_release); // B
// Hilo 2:
r1 = y.fetch_add(1, std::memory_order_seq_cst); // C
r2 = y.load(std::memory_order_relaxed); // D
// Hilo 3:
y.store(3, std::memory_order_seq_cst); // E
r3 = x.load(std::memory_order_seq_cst); // F

puede producir r1 == 1 && r2 == 3 && r3 == 0 , donde A precede-en-tiempo a C, pero C precede a A en el único orden total C-E-F-A de memory_order_seq_cst (ver Lahav et al ).

Nótese que:

1) tan pronto como operaciones atómicas que no están etiquetadas como memory_order_seq_cst entran en escena, se pierde la garantía de consistencia secuencial para el programa,
2) en muchos casos, las operaciones atómicas memory_order_seq_cst pueden reordenarse con respecto a otras operaciones atómicas realizadas por el mismo hilo.
(desde C++20)

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.

Este ejemplo demuestra una situación donde el orden secuencial es necesario. Cualquier otro orden podría activar el assert porque sería posible que los hilos c y d observen los cambios en los atómicos x y y en orden opuesto.

#include <atomic>
#include <cassert>
#include <thread>
std::atomic<bool> x = {false};
std::atomic<bool> y = {false};
std::atomic<int> z = {0};
void write_x()
{
    x.store(true, std::memory_order_seq_cst);
}
void write_y()
{
    y.store(true, std::memory_order_seq_cst);
}
void read_x_then_y()
{
    while (!x.load(std::memory_order_seq_cst))
        ;
    if (y.load(std::memory_order_seq_cst))
        ++z;
}
void read_y_then_x()
{
    while (!y.load(std::memory_order_seq_cst))
        ;
    if (x.load(std::memory_order_seq_cst))
        ++z;
}
int main()
{
    std::thread a(write_x);
    std::thread b(write_y);
    std::thread c(read_x_then_y);
    std::thread d(read_y_then_x);
    a.join(); b.join(); c.join(); d.join();
    assert(z.load() != 0); // will never happen
}

Relación con volatile

Dentro de un hilo de ejecución, los accesos (lecturas y escrituras) a través de volatile glvalues no pueden ser reordenados más allá de efectos secundarios observables (incluyendo otros accesos volátiles) que estén sequenced-before o sequenced-after 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átil pueden reordenarse libremente alrededor del acceso volátil).

Una excepción notable es Visual Studio, donde, con la configuración predeterminada, cada escritura volátil tiene semántica de liberación y cada lectura volátil tiene semántica de adquisición ( Microsoft Docs ), y por lo tanto los volátiles pueden usarse para sincronización entre hilos. La semántica estándar de volatile no es aplicable a programación multiproceso, aunque son suficientes para, por ejemplo, comunicación con un std::signal handler 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.

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. 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