Espacios de nombres
Variantes
Acciones

std::memory_order

De cppreference.com
< cpp‎ | atomic
 
 
 
Definido en el archivo de encabezado <atomic>
typedef enum memory_order {

    memory_order_relaxed,
    memory_order_consume,
    memory_order_acquire,
    memory_order_release,
    memory_order_acq_rel,
    memory_order_seq_cst

} memory_order;
(desde C++11)
(hasta C++20)
enum class memory_order : /*sin especificar*/ {

    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 los accesos de memoria, incluyendo accesos no atómicos regulares, deben ordenarse en torno a una operación atómica. En ausencia de restricciones en un sistema multinúcleo, cuando varios hilos leen y escriben simultáneamente en varias variables, un hilo puede observar que los valores cambian en un orden diferente del orden en que otro hilo los escribió. De hecho, el orden aparente de los cambios puede incluso diferir entre múltiples hilos de lectura. Algunos efectos similares pueden ocurrir incluso en sistemas uniprocesadores debido a las transformaciones del compilador permitidas por el modelo de memoria.

El comportamiento predeterminado de todas las operaciones atómicas en biblioteca proporciona un "orden secuencialmente consistente" (véase la discusión a continuación). Ese valor predeterminado puede afectar el rendimiento, pero las operaciones atómicas de la biblioteca pueden recibir un argumento std::memory_order para especificar las restricciones exactas, más allá de la atomicidad, que el compilador y el procesador deben imponer para esa operación.

Contenido

[editar] Constantes

Definido en el archivo de encabezado <atomic>
Valor Explicación
memory_order_relaxed Operación relajada: no hay restricciones de sincronización u ordenamiento impuestas en otras lecturas o escrituras, solo se garantiza la atomicidad de esta operación (véase Ordenamiento relajado a continuación).
memory_order_consume Una operación de carga con este orden de memoria realiza una operación de consumo en la ubicación de memoria afectada: no se pueden reordenar lecturas o escrituras en el hilo actual que dependen del valor actualmente cargado antes de esta carga. Las escrituras en 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 las plataformas, esto afecta solo a las optimizaciones del compilador (véase Ordenamiento liberar-consumir a continuación).
memory_order_acquire Una operación de carga con este orden de memoria realiza la "operación de toma" en la ubicación de memoria afectada: no se pueden reordenar lecturas o escrituras en el hilo actual antes de esta carga. Todas las escrituras en otros hilos que liberan la misma variable atómica son visibles en el hilo actual (véase Ordenamiento liberar-tomar a continuación).
memory_order_release Una operación de almacenamiento con este orden de memoria realiza la operación de liberación: no se pueden reordenar lecturas o escrituras en el hilo actual después de este almacenamiento. Todas las escrituras en el hilo actual son visibles en otros hilos que toman la misma variable atómica (véase Ordenamiento liberar-tomar a continuación) y las escrituras que llevan una dependencia a la variable atómica se hacen visibles en otros hilos que consumen la misma variable atómica (véase Ordenamiento liberar-consumir a continuación).
memory_order_acq_rel Una operación de lectura-modificación-escritura con este orden de memoria es tanto una operación de toma como una operación de liberación. No se pueden reordenar lecturas o escrituras de memoria en el hilo actual antes o después de este 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 toman la misma variable atómica.
memory_order_seq_cst Una operación de carga con este orden de memoria realiza una operación de toma, un almacenamiento realiza una operación de liberación y la lectura-modificación-escritura realiza tanto una operación de toma como una operación de liberación, además existe un único orden total en el que todos los hilos observan todas las modificaciones en el mismo orden (véase Ordenamiento secuencialmente consistente a continuación).

[editar] Descripción formal

La sincronización entre hilos y el orden de la 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:

[editar] Secuenciado-antes/secuenciada-antes

El término secuenciado-antes se aplica en su mayoría a evaluaciones, donde usaremos el término en femenino, secuenciada-antes. Donde el género de la aplicación sea masculino, usaremos secuenciado-antes. El término en inglés es sequenced-before, que no tiene género. Aunque una traducción sin género podría ser "se secuencia antes", solamente se puede usar en tiempo presente.

Dentro del mismo hilo, la evaluación A puede ser secuenciada-antes que la evaluación B, como se describe en el orden de evaluación.

[editar] Conlleva dependencia

Dentro del mismo hilo, la evaluación A que está secuenciada-antes de la evaluación B también puede conllevar una dependencia a B (es decir, B depende de A), si alguno de los siguientes es verdadero:

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

[editar] Orden de modificación

Todas las modificaciones a cualquier variable atómica particular ocurren en un orden total que es específico de dicha variable atómica. Los siguientes cuatro requisitos se garantizan para todas las operaciones atómicas:

1) Coherencia de escritura-escritura: si la evaluación A que modifica a un objeto atómico M (una escritura) sucede-antes que la evaluación B que modifica a M, entonces A aparece antes que B en el orden de modificación de M.
2) Coherencia de lectura-lectura: si un cálculo de valor A de un objeto atómico M (una lectura) sucede-antes que un cálculo de valor B en M, y si el valor de A proviene de una escritura X en M, entonces el valor de B es bien el valor almacenado por X, o el valor almacenado por un efecto secundario Y en M que aparece después que X en el orden de modificación de M.
3) Coherencia de lectura-escritura: si un cálculo de valor A de un objeto atómico M (una lectura) sucede-antes que una operación B en 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 sucede-antes que un cálculo de valor (una lectura) B de M, entonces la evaluación B deberá tomar su valor de X o del efecto secundario Y que sigue a X en el orden de modificación de M.

[editar] Secuencia de liberación

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

1) escrituras realizadas por el mismo hilo que realizó a A
(hasta C++20)
2) operaciones de lectura-modificación-escritura atómicas hechas a M por cualquier hilo

se le conoce como la secuencia de liberación encabezada por A.

[editar] Ordenada-por-dependencia antes

Entre hilos, la evaluación A está ordenada-por-dependencia antes que la evaluación B si cualquiera de los siguientes es verdadero:

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

[editar] Intrahilos sucede-antes

Entre hilos, la evaluación A intrahilos sucede antes que la evaluación de B si cualquiera de los siguientes es verdadero:

1) A se sincroniza-con B.
2) A está ordenada-por-dependencia antes que B.
3) A se sincroniza-con alguna evaluación X, y X está secuenciada-antes que B.
4) A está secuenciada-antes que alguna evaluación X, y X intrahilos sucede-antes que B.
5) A intrahilos sucede-antes que alguna evaluación X, y X intrahilos sucede-antes que B.

[editar] Sucede-antes

Independientemente de los hilos, la evaluación A sucede-antes que la evaluación B si cualquiera de los siguientes es verdadero:

1) A está secuenciada-antes que B.
2) A intrahilos sucede-antes que B.

Se requiere que la implementación asegure que la relación sucede-antes sea acíclica, mediante la introducción de sincronización adicional si es necesario (solamente puede ser necesario si una operación de consumo está envuelta; véase 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 carrera de datos) a menos que exista una relación sucede-antes entre estas dos evaluaciones.

Simplemente sucede-antes

Independientemente de los hilos, la evaluación A simplemente sucede-antes que la evaluación B si cualquiera de los siguientes es verdadero:

1) A está secuenciada-antes que B.
2) A se sincroniza-con B.
3) A simplemente sucede-antes que X, y X simplemente sucede-antes que B.

Nota: sin operaciones de consumo, las relaciones simplemente sucede-antes y sucede-antes son iguales.

(desde C++20)

[editar] Fuertemente sucede-antes

Independientemente de los hilos, la evaluación A fuertemente sucede-antes que la evaluación B si cualquiera de los siguientes es verdadero:

1) A está secuenciada-antes que B.
2) A se sincroniza-con B.
3) A fuertemente sucede-antes que X, y X fuertemente sucede-antes que B.
(hasta C++20)
1) A está secuenciada-antes que B
2) A se sincroniza-con B, y tanto A como B son operaciones atómicas secuencialmente consistentes.
3) A está secuenciada-antes que X, X simplemente sucede-antes que Y, e Y está secuenciada-antes que B.
4) A fuertemente sucede-antes que X, y X fuertemente sucede-antes que B.

Nota: Informalmente, si A fuertemente sucede-antes que B, entonces A parece a ser evaluada antes que B en todos los contextos.

Nota: fuertemente sucede-antes excluye las operaciones de consumo.

(desde C++20)

[editar] Efectos secundarios visibles

El efecto secundario A en un escalar M (una escritura) es visible con respecto al cálculo de valor B en M (una lectura) si se cumple lo siguiente:

1) A sucede-antes que B.
2) No hay otro efecto secundario X en M donde A sucede-antes que X y X sucede-antes que B.

Si el efecto secundario es visible con respecto al cálculo de valor B, entonces el subconjunto contiguo más largo de los efectos secundarios en M, en orden de modificación, donde B no sucede-antes, se le 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). la sincronización entre hilos se reduce a prevenir las carreras de datos (estableciendo relaciones sudede-antes) y definiendo qué efectos secundarios se hacen visibles bajo qué condiciones. Nota: la sincronización entre hilos se reduce a prevenir las carreras de datos (estableciendo relaciones sudede-antes) y definiendo qué efectos secundarios se hacen visibles bajo qué condiciones.

[editar] Operación de consumo

La carga atómica con memory_order_consume o más rígida es una operación de consumo. Observa que std::atomic_thread_fence impone requisitos de sincronización más rígidos que una operación de consumo.

[editar] Operación de toma

La carga atómica con memory_order_acquire o más rígida es una operación de toma. La operación lock() en un Mutex también es una operación de toma. Observa que std::atomic_thread_fence impone requisitos de sincronización más rígidos que una operación de toma.

[editar] Operación de liberación

La carga atómica con memory_order_release o más rígida es una operación de liberación. La operación unlock() en un Mutex también es una operación de liberación. Observa que std::atomic_thread_fence impone requisitos de sincronización más rígidos que una operación de liberación.

[editar] Explicación

[editar] Ordenamiento relajado

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

Por ejemplo, con x e 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 que se genere r1 == r2 == 42 porque, aunque A está secuenciada-antes que B dentro del hilo 1 y C está secuenciada-antes que 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 a la carga A en el hilo 1 mientras que el efecto secundario de B en x podría ser visible a la carga C en el hilo 2. En particular, esto puede suceder si D se completa antes que C en el hilo 2, bien debido a reordenamiento del compilador o en tiempo de ejecución.

Aun con el modelo de memoria relajado, no se permite que valores arbitrarios tengan una dependencia circular con sus propios cálculos. Por ejempo, con x e y inicialmente en cero:

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

no se permite que se genere r1 == r2 == 42 ya que el almacenamiento de 42 a y solo es posible si el almacenamiento a x almacena 42, lo que tiene una dependencia circular en el almacenamiento a y almacenando 42. Observa que hasta C++14, esto se permitía técnicamente por la especificación, pero no se recomendaba para los implementadores.

(desde C++14)

El uso típico para el ordenamiento de memoria relajado es el incremento de contadores, tales como los contadores de referencias de std::shared_ptr, ya que esto solamente requiere atomicidad, pero no ordenamiento o sincronización (observa que decrementar los contadores de shared_ptr requiere sincronización tomar-liberar con el destructor).

#include <vector>
#include <iostream>
#include <thread>
#include <atomic>
 
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 << "Valor final del contador es " << cnt << '\n';
}

Salida:

Valor final del contador es 10000


[editar] Ordenamiento liberar-tomar

Si almacenamiento atómico en el hilo A está etiquetado con memory_order_release y una carga atómica en el hilo B de la misma variable está etiquetada con memory_order_acquire, todas las escrituras a memoria (no atómicas y relajadas) que sucedieron-antes del almacenamiento atómico desde el punto de vista del hilo A, se convierten en efectos secundarios visibles 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 memoria.

La sincronización se establece solo entre hilos que liberan y toman la misma variable atómica. Otros hilos pueden ver un orden diferente de acceso a memoria que uno o ambos hilos sincronizados.

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

Cerrojos de exclusión mutua, como std::mutex o espín atómico, son un ejemplo de sincronización de liberar-tomar: cuando el hilo A libera el cerrojo y el hilo B lo toma, todo lo que tuvo lugar 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 toma) que está ejecutando la misma sección crítica .

#include <thread>
#include <atomic>
#include <cassert>
#include <string>
 
std::atomic<std::string*> ptr;
int datos;
 
void productor()
{
    std::string* p  = new std::string("Hola");
    datos = 42;
    ptr.store(p, std::memory_order_release);
}
 
void consumidor()
{
    std::string* p2;
    while (!(p2 = ptr.load(std::memory_order_acquire)))
        ;
    assert(*p2 == "Hola"); // nunca se dispara
    assert(datos == 42);   // nunca se dispara
}
 
int main()
{
    std::thread t1(productor);
    std::thread t2(consumidor);
    t1.join(); t2.join();
}


El siguiente ejemplo demuestra el ordenamiento liberar-adquirir transitivo a lo largo de tres hilos:

#include <thread>
#include <atomic>
#include <cassert>
#include <vector>
 
std::vector<int> datos;
std::atomic<int> bandera = {0};
 
void hilo_1()
{
    datos.push_back(42);
    bandera.store(1, std::memory_order_release);
}
 
void hilo_2()
{
    int esperado=1;
    while (!bandera.compare_exchange_strong(esperado, 2, std::memory_order_acq_rel)) {
        esperado = 1;
    }
}
 
void hilo_3()
{
    while (bandera.load(std::memory_order_acquire) < 2)
        ;
    assert(datos.at(0) == 42); // nunca se disparará
}
 
int main()
{
    std::thread a(hilo_1);
    std::thread b(hilo_2);
    std::thread c(hilo_3);
    a.join(); b.join(); c.join();
}


[editar] Ordenamiento liberar-consumir

Si un almacenamiento atómico en el hilo A está etiquetado con memory_order_release y una carga atómica en el hilo B de la misma variable que lee el valor almacenado está etiquetado con memory_order_consume, todas las escrituras a memoria (no atómicas y atómicas relajadas) que sucedieron antes del almacenamiento atómico desde el punto de vista del hilo A, se vuelven efectos secundarios visibles dentro de esas operaciones en el hilo B en el que la operación de carga conlleva dependencia, es decir, una vez que se completa la carga atómica, se garantiza que aquellos operadores y funciones en el hilo B que usan el valor obtenido de la carga verán lo que el hilo A escribió en la memoria.

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

En todas las CPU convencionales que no sean DEC Alpha, el orden de dependencia es automático, no se emiten instrucciones de CPU adicionales para este modo de sincronización, solo se ven afectadas ciertas optimizaciones del compilador (por ejemplo, el compilador tiene prohibido realizar cargas especulativas en los objetos involucrados en la dependencia cadena).

Los casos de uso típicos para este orden implican el 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 puntero, es decir, cuando el productor publica un puntero a través del cual el consumidor puede acceder a la información: no hay necesidad de hacer que todo lo demás que el productor escribió en la memoria sea visible para el consumidor (que 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 el control de la cadena de dependencias más a detalle.

Observa que actualmente (2/2015) ningún compilador de producción conocido rastrea las cadenas de dependencia: las operaciones de consumo se promueven a operaciones de toma.

La especificación del ordenamiento liberar-consumir se está revisando, y el uso de memory_order_consume por lo pronto no se recomienda.

(desde C++17)

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

#include <thread>
#include <atomic>
#include <cassert>
#include <string>
 
std::atomic<std::string*> ptr;
int datos;
 
void productor()
{
    std::string* p  = new std::string("Hola");
    datos = 42;
    ptr.store(p, std::memory_order_release);
}
 
void consumidor()
{
    std::string* p2;
    while (!(p2 = ptr.load(std::memory_order_consume)))
        ;
    assert(*p2 == "Hola"); // nunca se dispara: *p2 conlleva dependencia de ptr
    assert(datos == 42);   // puede o no dispararse:
                           // datos no conlleva dependencia de ptr
}
 
int main()
{
    std::thread t1(productor);
    std::thread t2(consumidor);
    t1.join(); t2.join();
}


[editar] Ordenamiento secuencialmente consistente

Las operaciones atómicas etiquetadas con memory_order_seq_cst no solamente ordenan memoria de la misma manera que el ordenamiento liberar-tomar (todo lo que sucedió-antes que un almacenamiento en un hilo se vuelve un efecto secundario visible en el hilo que hizo la carga), sino también establecen un orden de modificación total único de todas las operaciones atómicas que están etiquetadas de la misma manera.

Formalmente,

Cada operación B memory_order_seq_cst que se carga desde la variable atómica M, observa uno de los siguientes:

  • el resultado de la última operación A que modificó M, que aparece antes que B en el orden total único;
  • o, si existiera tal A, B puede observar el resultado de alguna modificación en M que no sea memory_order_seq_cst y no sucede-antes que A;
  • o, si no hubiera una A, B puede observar el resultado de alguna modificación no relacionada de M que no sea memory_order_seq_cst.

Si hubo una operación X memory_order_seq_cst {{# ifeq:cpp|cpp|std::}}atomic_thread_fence secuenciada-antes que B, entonces B observa uno de los siguientes:

  • la última modificación memory_order_seq_cst de M que aparece antes que X en el orden total único;
  • alguna modificación no relacionada de M que aparece más adelante 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 hay dos memory_order_seq_cst std::atomic_thread_fences X e Y, y si A está secuenciada-antes que X, Y está secuenciada-antes que 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:

  • hay una memory_order_seq_cst std::atomic_thread_fence X tal que A está secuenciada-antes que X, y X aparece antes que B en el orden total único;
  • o, hay una memory_order_seq_cst std::atomic_thread_fence Y tal que Y está secuenciada-antes que B, y A aparece antes que Y en el orden total único;
  • o, hay memory_order_seq_cst std::atomic_thread_fences X e Y de tal manera que A está secuenciada-antes que X, Y está secuenciada-antes que B, y X aparece antes que Y en el orden total único.

Observa que esto significa que:

1) tan pronto como las operaciones atómicas que no están etiquetadas con memory_order_seq_cst entren en escena, se pierde la coherencia 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 (secuenciada-antes no es una relación entre hilos, a diferencia de sucede-antes).
(hasta C++20)
Formalmente,

Una operación atómica A en algún objeto atómico M está coherencia-ordenada-antes que otra operación atómica B en M si se cumple cualquiera 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á coherencia-ordenada-antes que X, y X está coherencia-ordenada-antes que B.

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

1) si A y B son operaciones memory_order_seq_cst, y A fuertemente-sucede-antes que B, entonces A precede a B en S;
2) para cada par de operaciones atómicas A y B en un objeto M, donde A está coherencia-ordenada-antes que B:
a) si A y B son 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 sucede-antes que una barrera memory_order_seq_cst Y, entonces A precede a Y en S;
c) si una barrera memory_order_seq_cst X sucede-antes que 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 sucede-antes que A, y B sucede-antes que una barrera memory_order_seq_cst Y, entonces X precede a Y en S.

La definición formal asegura que:

1) el orden total único es consistente con el "orden de modificación" de cualquier objeto atómico;
2) una carga memory_order_seq_cst obtiene su valor de la última modificación memory_order_seq_cst, o de alguna modificación que no sea memory_order_seq_cst que no "sucede-antes" que las modificaciones memory_order_seq_cst anteriores.

El orden total único puede no ser consistente con "sucede-antes". Esto permite una implementación más eficiente de memory_order_acquire y memory_order_release en algunas CPU. 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 en 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

se le permite producir r1 == 1 && r2 == 3 && r3 == 0, donde A sucede-antes que C, pero C precede a A en el orden C-E-F-A total único de memory_order_seq_cst (véase Lahav et al).

Observa que:

1) tan pronto como las operaciones atómicas que no están etiquetadas con memory_order_seq_cst entren en escena, se pierde la garantía de coherencia 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)

El ordenamiento secuencial puede ser necesario para situaciones de múltiples productores-múltiples consumidores donde todos los consumidores deben observar que las acciones de todos los productores ocurran en el mismo orden.

El ordenamiento secuencial requiere una instrucción de CPU de barrera de memoria total en todos los sistemas multinúcleo. Esto puede convertirse en un cuello de botella, ya que fuerza a que los accesos de memoria afectados se propagen a cada núcleo.

Este ejemplo demuestra una situación donde el ordenamiento secuencial es necesario. Cualquier otro ordenamiento puede disparar la aserción porque sería posible para los hilos c y d para observar cambios en las variables atómicas x e y en orden inverso.

#include <thread>
#include <atomic>
#include <cassert>
 
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);  // nunca sucederá
}


[editar] Relación con volatile

Dentro de un hilo de ejecución, accesos (lecturas y escrituras) a través de glvalues volátiles no se pueden reordenar más allá de los efectos secundarios observables (incluidos otros accesos volátiles) que están secuenciados-antes o secuenciados-después dentro del mismo hilo, pero no se garantiza que este orden sea observado por otro hilo, ya que el acceso volátil no establece la sincronización entre hilos.

Además, los accesos volátiles no son atómicos (la lectura y escritura concurrente es una carrera de datos) y no ordena la memoria (los accesos de memoria no volátiles pueden reordenarse libremente en torno al 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 toma. (MSDN) y, por lo tanto, los volátiles pueden usarse para la sincronización entre hilos. La semántica estándar volatile no es aplicable a la programación multiproceso, aunque son suficientes para, p. ej., comunicación con un controlador de señales (std::signal) que se ejecuta en el mismo hilo cuando se aplica a variables sig_atomic_t.

[editar] Véase también

Documentación de C para memory order

[editar] Enlaces externos