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
Definiert im Header <atomic>
enum memory_order

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

} ;
(seit C++11)
(bis C++20)
enum class memory_order : /* nicht spezifiziert */

{
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 ;
(seit C++20)

std::memory_order gibt an, wie Speicherzugriffe, einschließlich regulärer, nicht-atomarer Speicherzugriffe, um eine atomare Operation herum angeordnet werden sollen. Ohne jegliche Einschränkungen in einem Mehrkernsystem können, wenn mehrere Threads gleichzeitig auf mehrere Variablen lesend und schreibend zugreifen, die Werteänderungen in einer anderen Reihenfolge beobachtet werden, als sie von einem anderen Thread geschrieben wurden. Tatsächlich kann die scheinbare Reihenfolge der Änderungen sogar zwischen mehreren Leser-Threads variieren. Einige ähnliche Effekte können sogar auf Einprozessorsystemen aufgrund von Compiler-Transformationen auftreten, die durch das Speichermodell erlaubt sind.

Das Standardverhalten aller atomaren Operationen in der Bibliothek bietet sequentiell konsistente Reihenfolge (siehe Diskussion unten). Diese Standardeinstellung kann die Leistung beeinträchtigen, aber den atomaren Operationen der Bibliothek kann ein zusätzliches std::memory_order -Argument übergeben werden, um die genauen Einschränkungen anzugeben, die der Compiler und der Prozessor für diese Operation zusätzlich zur Atomarität erzwingen müssen.

Inhaltsverzeichnis

Konstanten

Definiert im Header <atomic>
Name Bedeutung
memory_order_relaxed Entspannte Operation: Es werden keine Synchronisierungs- oder Reihenfolgebeschränkungen für andere Lese- oder Schreibvorgänge auferlegt, nur die Atomarität dieser Operation ist garantiert (siehe Entspannte Reihenfolge unten).
memory_order_consume
(veraltet in C++26)
Ein Ladevorgang mit dieser Speicherreihenfolge führt einen Consume-Vorgang auf der betroffenen Speicherstelle aus: Keine Lese- oder Schreibvorgänge im aktuellen Thread, die vom aktuell geladenen Wert abhängen, können vor diesen Ladevorgang verschoben werden. Schreibvorgänge in anderen Threads, die dieselbe atomare Variable freigeben und datenabhängige Variablen betreffen, sind im aktuellen Thread sichtbar. Auf den meisten Plattformen betrifft dies nur Compiler-Optimierungen (siehe Release-Consume-Reihenfolge unten).
memory_order_acquire Ein Ladevorgang mit dieser Speicherreihenfolge führt den Acquire-Vorgang auf der betroffenen Speicherstelle aus: Keine Lese- oder Schreibvorgänge im aktuellen Thread können vor diesen Ladevorgang verschoben werden. Alle Schreibvorgänge in anderen Threads, die dieselbe atomare Variable freigeben, sind im aktuellen Thread sichtbar (siehe Release-Acquire-Reihenfolge unten).
memory_order_release Ein Schreibvorgang mit dieser Speicherreihenfolge führt den Release-Vorgang aus: Keine Lese- oder Schreibvorgänge im aktuellen Thread können nach diesen Schreibvorgang verschoben werden. Alle Schreibvorgänge im aktuellen Thread sind in anderen Threads sichtbar, die dieselbe atomare Variable akquirieren (siehe Release-Acquire-Reihenfolge unten), und Schreibvorgänge, die eine Abhängigkeit in die atomare Variable tragen, werden in anderen Threads sichtbar, die dieselbe atomare Variable konsumieren (siehe Release-Consume-Reihenfolge unten).
memory_order_acq_rel Ein Lese-Modifizieren-Schreiben-Vorgang mit dieser Speicherreihenfolge ist sowohl ein Acquire-Vorgang als auch ein Release-Vorgang . Keine Speicher-Lese- oder Schreibvorgänge im aktuellen Thread können vor den Ladevorgang oder nach den Schreibvorgang verschoben werden. Alle Schreibvorgänge in anderen Threads, die dieselbe atomare Variable freigeben, sind vor der Modifikation sichtbar, und die Modifikation ist in anderen Threads sichtbar, die dieselbe atomare Variable akquirieren.
memory_order_seq_cst Ein Ladevorgang mit dieser Speicherreihenfolge führt einen Acquire-Vorgang aus, ein Schreibvorgang führt einen Release-Vorgang aus, und ein Lese-Modifizieren-Schreiben-Vorgang führt sowohl einen Acquire-Vorgang als auch einen Release-Vorgang aus, zusätzlich existiert eine einzige Gesamtreihenfolge, in der alle Threads alle Modifikationen in derselben Reihenfolge beobachten (siehe Sequentiell-konsistente Reihenfolge unten).

Formale Beschreibung

Thread-übergreifende Synchronisation und Speicherreihenfolge bestimmen, wie Auswertungen und Nebeneffekte von Ausdrücken zwischen verschiedenen Ausführungsthreads geordnet werden. Sie werden in folgenden Begriffen definiert:

Sequenced-before

Innerhalb desselben Threads kann Auswertung A sequenced-before Auswertung B sein, wie beschrieben in evaluation order .

Trägt Abhängigkeit

Innerhalb desselben Threads kann Auswertung A, die sequenced-before Auswertung B ist, auch eine Abhängigkeit in B tragen (d.h. B hängt von A ab), falls einer der folgenden Fälle zutrifft:

1) Der Wert von A wird als Operand von B verwendet, außer
a) wenn B ein Aufruf von std::kill_dependency ist,
b) wenn A der linke Operand der eingebauten && , || , ?: , oder , Operatoren ist.
2) A schreibt in ein skalares Objekt M, B liest aus M.
3) A trägt Abhängigkeit in eine andere Auswertung X, und X trägt Abhängigkeit in B.
(bis C++26)

Änderungsreihenfolge

Alle Änderungen an einer bestimmten atomaren Variable erfolgen in einer Gesamtordnung, die spezifisch für diese eine atomare Variable ist.

Die folgenden vier Anforderungen sind für alle atomaren Operationen garantiert:

1) Write-write coherence : Wenn Auswertung A, die ein atomares M modifiziert (ein Schreibvorgang) happens-before Auswertung B, die M modifiziert, dann erscheint A früher als B in der modification order von M.
2) Read-Read-Kohärenz : wenn eine Werteberechnung A eines atomaren M (ein Lesevorgang) happens-before einer Werteberechnung B auf M, und wenn der Wert von A von einem Schreibvorgang X auf M stammt, dann ist der Wert von B entweder der durch X gespeicherte Wert oder der durch einen Nebeneffekt Y auf M gespeicherte Wert, der später als X in der modification order von M erscheint.
3) Lese-Schreib-Kohärenz : Wenn eine Werteberechnung A eines atomaren M (ein Lesevorgang) happens-before einer Operation B auf M (ein Schreibvorgang), dann stammt der Wert von A aus einem Nebeneffekt (einem Schreibvorgang) X, der in der modification order von M vor B erscheint.
4) Schreib-Lese-Kohärenz : wenn ein Seiteneffekt (ein Schreibvorgang) X auf ein atomares Objekt M happens-before einer Wertermittlung (einem Lesevorgang) B von M, dann soll die Auswertung B ihren Wert von X oder von einem Seiteneffekt Y nehmen, der X in der Modifikationsreihenfolge von M folgt.

Release-Sequenz

Nach einem release operation A, der auf ein atomares Objekt M angewendet wird, ist die längste zusammenhängende Teilfolge der Modifikationsreihenfolge von M, die besteht aus:

1) Schreibvorgänge, die vom selben Thread ausgeführt werden, der A ausgeführt hat.
(bis C++20)
2) Atomare Lese-Modifizier-Schreib-Operationen, die von einem beliebigen Thread an M durchgeführt werden.

Wird bezeichnet als release sequence headed by A .

Synchronisiert mit

Wenn ein atomarer Speichervorgang in Thread A ein Release-Operation ist, ein atomarer Ladevorgang in Thread B von derselben Variable eine Acquire-Operation ist, und der Ladevorgang in Thread B einen durch den Speichervorgang in Thread A geschriebenen Wert liest, dann synchronisiert der Speichervorgang in Thread A mit dem Ladevorgang in Thread B.

Auch können einige Bibliotheksaufrufe so definiert sein, dass sie synchronize-with anderen Bibliotheksaufrufen in anderen Threads stehen.

Dependency-ordered before

Zwischen Threads ist Auswertung A dependency-ordered before Auswertung B, wenn eine der folgenden Bedingungen zutrifft:

1) A führt einen release operation auf einem atomaren M aus, und in einem anderen Thread führt B einen consume operation auf demselben atomaren M aus, und B liest einen Wert, der durch einen beliebigen Teil der Freigabesequenz geschrieben wurde (bis C++20) von A.
2) A ist dependency-ordered before X und X trägt eine Abhängigkeit in B.
(bis C++26)

Inter-thread Happens-Before

Zwischen Threads, evaluation A inter-thread happens before evaluation B, wenn eines der Folgenden zutrifft:

1) A synchronizes-with B.
2) A ist dependency-ordered before B.
3) A synchronizes-with einer Auswertung X, und X ist sequenced-before B.
4) A ist sequenced-before einer Auswertung X, und X inter-thread happens-before B.
5) A inter-thread happens-before einer Auswertung X, und X inter-thread happens-before B.


Happens-before

Unabhängig von Threads erfolgt Auswertung A happens-before Auswertung B, wenn eine der folgenden Bedingungen zutrifft:

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

Die Implementierung muss sicherstellen, dass die happens-before -Beziehung azyklisch ist, indem sie bei Bedarf zusätzliche Synchronisierung einführt (dies kann nur erforderlich sein, wenn eine Consume-Operation beteiligt ist, siehe Batty et al ).

Wenn eine Auswertung einen Speicherort modifiziert und eine andere denselben Speicherort liest oder modifiziert, und mindestens eine der Auswertungen kein atomarer Vorgang ist, ist das Verhalten des Programms undefiniert (das Programm hat einen Data Race ), es sei denn, es besteht eine happens-before -Beziehung zwischen diesen beiden Auswertungen.

Simply happens-before

Unabhängig von Threads erfolgt Auswertung A simply happens-before Auswertung B, wenn eine der folgenden Bedingungen zutrifft:

1) A ist sequenced-before B.
2) A synchronizes-with B.
3) A simply happens-before X und X simply happens-before B.

Hinweis: Ohne Consume-Operationen sind die Beziehungen simply happens-before und happens-before identisch.

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

Happens-before

Unabhängig von Threads erfolgt Auswertung A happens-before Auswertung B, wenn eine der folgenden Bedingungen zutrifft:

1) A ist sequenced-before B.
2) A synchronizes-with B.
3) A happens-before X und X happens-before B.
(since C++26)

Strongly happens-before

Unabhängig von Threads, erfolgt Auswertung A strongly happens-before Auswertung B, falls eines der folgenden zutrifft:

1) A ist sequenced-before B.
2) A synchronizes-with B.
3) A strongly happens-before X, und X strongly happens-before B.
(bis C++20)
1) A ist sequenced-before B.
2) A synchronizes with B, und sowohl A als auch B sind sequentiell konsistente atomare Operationen.
3) A ist sequenced-before X, X einfach (bis C++26) happens-before Y, und Y ist sequenced-before B.
4) A strongly happens-before X, und X strongly happens-before B.

Hinweis: informell gilt, wenn A strongly happens-before B, dann scheint A in allen Kontexten vor B ausgewertet zu werden.

Hinweis: strongly happens-before schließt consume-Operationen aus.

(bis C++26)
(seit C++20)

Sichtbare Nebeneffekte

Der Nebeneffekt A auf einen Skalar M (ein Schreibvorgang) ist sichtbar in Bezug auf die Wertermittlung B auf M (ein Lesevorgang), wenn beide der folgenden Bedingungen zutreffen:

1) A happens-before B.
2) Es gibt keinen anderen Nebeneffekt X zu M, bei dem A happens-before X und X happens-before B.

Wenn Nebeneffekt A in Bezug auf die Wertberechnung B sichtbar ist, dann ist die längste zusammenhängende Teilmenge der Nebeneffekte auf M in modification order , bei der B nicht happen-before dazu steht, bekannt als die visible sequence of side-effects (der Wert von M, bestimmt durch B, wird der durch einen dieser Nebeneffekte gespeicherte Wert sein).

Hinweis: Die Synchronisation zwischen Threads läuft darauf hinaus, Datenrennen zu verhindern (durch das Herstellen von Happens-Before-Beziehungen) und festzulegen, welche Nebeneffekte unter welchen Bedingungen sichtbar werden.

Verbrauchsvorgang

Atomarer Ladevorgang mit memory_order_consume oder stärker ist ein Consume-Operation. Beachten Sie, dass std::atomic_thread_fence stärkere Synchronisierungsanforderungen als eine Consume-Operation stellt.

Acquire-Operation

Atomarer Ladevorgang mit memory_order_acquire oder stärker ist ein Acquire-Operation. Der lock() -Vorgang auf einem Mutex ist ebenfalls eine Acquire-Operation. Beachten Sie, dass std::atomic_thread_fence stärkere Synchronisierungsanforderungen auferlegt als eine Acquire-Operation.

Freigabeoperation

Atomic Store mit memory_order_release oder stärker ist ein Release-Vorgang. Die unlock() -Operation auf einem Mutex ist ebenfalls ein Release-Vorgang. Beachten Sie, dass std::atomic_thread_fence stärkere Synchronisierungsanforderungen als ein Release-Vorgang auferlegt.

Erklärung

Relaxierte Reihenfolge

Atomare Operationen, die mit memory_order_relaxed gekennzeichnet sind, sind keine Synchronisationsoperationen; sie erzwingen keine Reihenfolge zwischen gleichzeitigen Speicherzugriffen. Sie garantieren lediglich Atomarität und Konsistenz der Modifikationsreihenfolge.

Zum Beispiel, mit x und y anfänglich null,

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

ist es erlaubt, r1 == r2 == 42 zu produzieren, weil, obwohl A sequenced-before B in Thread 1 ist und C sequenced before D in Thread 2 ist, nichts verhindert, dass D vor A in der Modifikationsreihenfolge von y erscheint und B vor C in der Modifikationsreihenfolge von x erscheint. Der Seiteneffekt von D auf y könnte für den Ladevorgang A in Thread 1 sichtbar sein, während der Seiteneffekt von B auf x für den Ladevorgang C in Thread 2 sichtbar sein könnte. Insbesondere kann dies auftreten, wenn D vor C in Thread 2 abgeschlossen wird, entweder aufgrund von Compiler-Neuanordnungen oder zur Laufzeit.

Selbst mit einem entspannten Speichermodell ist es nicht erlaubt, dass aus dem Nichts erscheinende Werte zirkulär von ihren eigenen Berechnungen abhängen, zum Beispiel, wenn x und y anfänglich null sind,

// 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);

ist nicht erlaubt, r1 == r2 == 42 zu erzeugen, da das Speichern von 42 in y nur möglich ist, wenn das Speichern in x 42 speichert, was zirkulär vom Speichern von 42 in y abhängt. Zu beachten ist, dass dies bis C++14 technisch durch die Spezifikation erlaubt war, aber für Implementierer nicht empfohlen wurde.

(since C++14)

Typische Verwendung für entspannte Speicherordnung ist das Inkrementieren von Zählern, wie etwa die Referenzzähler von std::shared_ptr , da dies lediglich Atomarität erfordert, jedoch keine Reihenfolge oder Synchronisation (beachte, dass das Dekrementieren der std::shared_ptr -Zähler eine Acquire-Release-Synchronisation mit dem Destruktor erfordert).

#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';
}

Ausgabe:

Final counter value is 10000

Release-Acquire-Reihenfolge

Wenn ein atomarer Speichervorgang in Thread A mit memory_order_release gekennzeichnet ist, ein atomarer Ladevorgang in Thread B von derselben Variable mit memory_order_acquire gekennzeichnet ist, und der Ladevorgang in Thread B einen durch den Speichervorgang in Thread A geschriebenen Wert liest, dann synchronisiert der Speichervorgang in Thread A mit dem Ladevorgang in Thread B.

Alle Speicherschreibvorgänge (einschließlich nicht-atomarer und entspannter atomarer), die aus Sicht von Thread A happened-before dem atomaren Speichervorgang liegen, werden in Thread B zu visible side-effects . Das heißt, sobald der atomare Ladevorgang abgeschlossen ist, ist garantiert, dass Thread B alle Speicherschreibvorgänge von Thread A sieht. Diese Zusicherung gilt nur, wenn B tatsächlich den von A gespeicherten Wert oder einen Wert aus später in der Release-Sequenz zurückgibt.

Die Synchronisierung wird nur zwischen den Threads hergestellt, die releasing und acquiring derselben atomaren Variable durchführen. Andere Threads können eine unterschiedliche Reihenfolge von Speicherzugriffen sehen als einer oder beide der synchronisierten Threads.

Auf stark geordneten Systemen — x86, SPARC TSO, IBM Mainframe, etc. — ist Release-Acquire-Reihenfolge für die meisten Operationen automatisch. Für diesen Synchronisationsmodus werden keine zusätzlichen CPU-Befehle ausgegeben; nur bestimmte Compiler-Optimierungen sind betroffen (z.B. ist es dem Compiler untersagt, nicht-atomare Speichervorgänge über den atomaren Store-Release zu verschieben oder nicht-atomare Ladevorgänge früher als den atomaren Load-Acquire durchzuführen). Auf schwach geordneten Systemen (ARM, Itanium, PowerPC) werden spezielle CPU-Lade- oder Memory-Fence-Befehle verwendet.

Gegenseitige Ausschlusssperren, wie std::mutex oder atomare Spinlock , sind ein Beispiel für Release-Acquire-Synchronisation: Wenn die Sperre von Thread A freigegeben und von Thread B erworben wird, muss alles, was im kritischen Abschnitt (vor der Freigabe) im Kontext von Thread A stattgefunden hat, für Thread B (nach dem Erwerb) sichtbar sein, der denselben kritischen Abschnitt ausführt.

#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"); // never fires
    assert(data == 42); // never fires
}
int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join(); t2.join();
}

Das folgende Beispiel demonstriert transitive Release-Acquire-Ordnung über drei Threads hinweg, unter Verwendung einer Release-Sequenz.

#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 ist in Ordnung, da dies ein RMW ist,
    // und RMWs (mit beliebiger Ordnung) nach einem Release bilden eine Release-Sequenz
    while (!flag.compare_exchange_strong(expected, 2, std::memory_order_relaxed))
    {
        expected = 1;
    }
}
void thread_3()
{
    while (flag.load(std::memory_order_acquire) < 2)
        ;
    // wenn wir den Wert 2 vom atomaren Flag lesen, sehen wir 42 im Vektor
    assert(data.at(0) == 42); // wird niemals fehlschlagen
}
int main()
{
    std::thread a(thread_1);
    std::thread b(thread_2);
    std::thread c(thread_3);
    a.join(); b.join(); c.join();
}

Release-Consume-Reihenfolge

Wenn ein atomarer Store in Thread A mit memory_order_release gekennzeichnet ist, ein atomarer Load in Thread B von derselben Variable mit memory_order_consume gekennzeichnet ist, und der Load in Thread B einen Wert liest, der durch den Store in Thread A geschrieben wurde, dann ist der Store in Thread A dependency-ordered before dem Load in Thread B.

Alle Speicherschreibvorgänge (nicht-atomar und relaxed atomar), die aus Sicht von Thread A happened-before dem atomaren Store waren, werden zu visible side-effects innerhalb jener Operationen in Thread B, in die der Load-Vorgang carries dependency trägt, das heißt, sobald der atomare Load abgeschlossen ist, ist garantiert, dass jene Operatoren und Funktionen in Thread B, die den vom Load erhaltenen Wert verwenden, sehen, was Thread A in den Speicher geschrieben hat.

Die Synchronisierung wird nur zwischen den Threads hergestellt, die dieselbe atomare Variable releasing und consuming . Andere Threads können eine andere Reihenfolge von Speicherzugriffen sehen als einer oder beide der synchronisierten Threads.

Auf allen gängigen CPUs außer DEC Alpha ist die Abhängigkeitsordnung automatisch, für diesen Synchronisierungsmodus werden keine zusätzlichen CPU-Befehle ausgegeben, nur bestimmte Compiler-Optimierungen sind betroffen (z.B. ist es dem Compiler untersagt, spekulative Loads auf den Objekten durchzuführen, die an der Abhängigkeitskette beteiligt sind).

Typische Anwendungsfälle für diese Ordnung umfassen Lesezugriff auf selten geschriebene nebenläufige Datenstrukturen (Routing-Tabellen, Konfiguration, Sicherheitsrichtlinien, Firewall-Regeln etc.) und Publisher-Subscriber-Situationen mit pointer-vermittelter Veröffentlichung, das heißt, wenn der Producer einen Pointer veröffentlicht, über den der Consumer auf Informationen zugreifen kann: Es ist nicht notwendig, alles andere, was der Producer in den Speicher geschrieben hat, für den Consumer sichtbar zu machen (was auf schwach geordneten Architekturen eine teure Operation sein kann). Ein Beispiel für ein solches Szenario ist rcu_dereference .

Siehe auch std::kill_dependency und [[ carries_dependency ]] für feinkörnige Abhängigkeitskettensteuerung.

Beachten Sie, dass derzeit (2/2015) keine bekannten Produktionscompiler Abhängigkeitsketten verfolgen: consume-Operationen werden zu acquire-Operationen heraufgestuft.

(bis C++26)

Die Spezifikation der Release-Consume-Reihenfolge wird überarbeitet, und die Verwendung von memory_order_consume wird vorübergehend nicht empfohlen.

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

Release-Consume-Reihenfolge hat denselben Effekt wie Release-Acquire-Reihenfolge und ist veraltet.

(since C++26)

Dieses Beispiel demonstriert abhängigkeitsgeordnete Synchronisierung für zeigervermittelte Veröffentlichung: Die Integer-Daten stehen mit dem Zeiger auf den String nicht in einer Datenabhängigkeitsbeziehung, daher ist ihr Wert im Consumer undefiniert.

#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();
}


Sequenziell konsistente Reihenfolge

Atomare Operationen, die mit memory_order_seq_cst gekennzeichnet sind, ordnen den Speicher nicht nur auf die gleiche Weise wie Release/Acquire-Ordnung (alles, was happened-before eines Speichers in einem Thread war, wird zu einem visible side effect im Thread, der einen Ladevorgang durchführte), sondern stellen auch eine single total modification order aller atomaren Operationen her, die so gekennzeichnet sind.

Formal gilt:

Jede memory_order_seq_cst -Operation B, die von der atomaren Variable M lädt, beobachtet eines der folgenden Szenarien:

  • das Ergebnis der letzten Operation A, die M modifiziert hat und die vor B in der einzigen Gesamtreihenfolge erscheint,
  • ODER, falls ein solches A existierte, kann B das Ergebnis einer Modifikation an M beobachten, die nicht memory_order_seq_cst ist und nicht happen-before A liegt,
  • ODER, falls kein solches A existierte, kann B das Ergebnis einer unabhängigen Modifikation von M beobachten, die nicht memory_order_seq_cst ist.

Falls eine memory_order_seq_cst std::atomic_thread_fence -Operation X sequenced-before B existiert, dann beobachtet B eines der folgenden Szenarien:

  • die letzte memory_order_seq_cst -Modifikation von M, die vor X in der einzigen Gesamtreihenfolge erscheint,
  • eine unabhängige Modifikation von M, die später in der Modifikationsreihenfolge von M erscheint.

Für ein Paar atomarer Operationen auf M namens A und B, wobei A schreibt und B den Wert von M liest, falls zwei memory_order_seq_cst std::atomic_thread_fence s X und Y existieren, und falls A sequenced-before X liegt, Y sequenced-before B liegt und X vor Y in der Einzigen Gesamtreihenfolge erscheint, dann beobachtet B entweder:

  • die Auswirkung von A,
  • eine unabhängige Modifikation von M, die nach A in der Modifikationsreihenfolge von M erscheint.

Für ein Paar atomarer Modifikationen von M namens A und B tritt B nach A in der Modifikationsreihenfolge von M auf, falls:

  • ein memory_order_seq_cst std::atomic_thread_fence X existiert, sodass A sequenced-before X liegt und X vor B in der Einzigen Gesamtreihenfolge erscheint,
  • oder, ein memory_order_seq_cst std::atomic_thread_fence Y existiert, sodass Y sequenced-before B liegt und A vor Y in der Einzigen Gesamtreihenfolge erscheint,
  • oder, memory_order_seq_cst std::atomic_thread_fence s X und Y existieren, sodass A sequenced-before X liegt, Y sequenced-before B liegt und X vor Y in der Einzigen Gesamtreihenfolge erscheint.

Beachten Sie, dass dies bedeutet:

1) sobald atomare Operationen, die nicht mit memory_order_seq_cst markiert sind, ins Spiel kommen, geht die sequenzielle Konsistenz verloren,
2) die sequenziell-konsistenten Fences stellen nur eine Gesamtreihenfolge für die Fences selbst her, nicht für die atomaren Operationen im Allgemeinen ( sequenced-before ist keine threadsübergreifende Beziehung, im Gegensatz zu happens-before ).
(bis C++20)
Formal gilt:

Eine atomare Operation A auf einem atomaren Objekt M ist coherence-ordered-before einer anderen atomaren Operation B auf M, wenn eine der folgenden Bedingungen zutrifft:

1) A ist eine Modifikation, und B liest den von A gespeicherten Wert,
2) A geht B in der modification order von M voraus,
3) A liest den von einer atomaren Modifikation X gespeicherten Wert, X geht B in der modification order voraus, und A und B sind nicht dieselbe atomare Lese-Modifizier-Schreib-Operation,
4) A ist coherence-ordered-before X, und X ist coherence-ordered-before B.

Es existiert eine einzige totale Ordnung S für alle memory_order_seq_cst -Operationen, einschließlich Fences, die folgenden Einschränkungen genügt:

1) wenn A und B memory_order_seq_cst -Operationen sind und A strongly happens-before B, dann geht A B in S voraus,
2) für jedes Paar atomarer Operationen A und B auf einem Objekt M, wobei A coherence-ordered-before B ist:
a) wenn A und B beide memory_order_seq_cst -Operationen sind, dann geht A B in S voraus,
b) wenn A eine memory_order_seq_cst -Operation ist und B happens-before einem memory_order_seq_cst -Fence Y, dann geht A Y in S voraus,
c) wenn ein memory_order_seq_cst -Fence X happens-before A und B eine memory_order_seq_cst -Operation ist, dann geht X B in S voraus,
d) wenn ein memory_order_seq_cst -Fence X happens-before A und B happens-before einem memory_order_seq_cst -Fence Y, dann geht X Y in S voraus.

Die formale Definition stellt sicher, dass:

1) die einzige totale Ordnung mit der modification order jedes atomaren Objekts konsistent ist,
2) ein memory_order_seq_cst -Ladevorgang seinen Wert entweder von der letzten memory_order_seq_cst -Modifikation oder von einer nicht- memory_order_seq_cst -Modifikation erhält, die nicht happen-before vorhergehenden memory_order_seq_cst -Modifikationen liegt.

Die einzige totale Ordnung muss nicht mit happens-before konsistent sein. Dies ermöglicht eine effizientere Implementierung von memory_order_acquire und memory_order_release auf einigen CPUs. Es kann überraschende Ergebnisse liefern, wenn memory_order_acquire und memory_order_release mit memory_order_seq_cst gemischt werden.

Beispielsweise dürfen mit x und y anfänglich null,

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

die Ergebnisse r1 == 1 && r2 == 3 && r3 == 0 erzeugt werden, wobei A happens-before C, aber C in der einzigen totalen Ordnung C-E-F-A der memory_order_seq_cst -Operationen A vorausgeht (siehe Lahav et al ).

Zu beachten ist:

1) sobald atomare Operationen, die nicht mit memory_order_seq_cst markiert sind, ins Spiel kommen, geht die sequentielle Konsistenzgarantie für das Programm verloren,
2) in vielen Fällen können memory_order_seq_cst -Operationen in Bezug auf andere atomare Operationen desselben Threads neu geordnet werden.
(seit C++20)

Eine sequenzielle Reihenfolge kann in Situationen mit mehreren Produzenten und mehreren Konsumenten erforderlich sein, bei denen alle Konsumenten die Aktionen aller Produzenten in derselben Reihenfolge wahrnehmen müssen.

Totale sequenzielle Ordnung erfordert eine vollständige Memory-Fence-CPU-Anweisung auf allen Multi-Core-Systemen. Dies kann zu einem Performance-Engpass werden, da es die betroffenen Speicherzugriffe zwingt, sich zu jedem Kern zu propagieren.

Dieses Beispiel demonstriert eine Situation, in der sequenzielle Ordnung notwendig ist. Jede andere Reihenfolge könnte die Assertion auslösen, da es möglich wäre, dass die Threads c und d Änderungen an den atomaren Variablen x und y in entgegengesetzter Reihenfolge beobachten.

#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
}

Beziehung zu volatile

Innerhalb eines Ausführungsstrangs können Zugriffe (Lese- und Schreibvorgänge) über volatile glvalues nicht über beobachtbare Seiteneffekte (einschließlich anderer volatile-Zugriffe) verschoben werden, die sequenced-before oder sequenced-after im selben Thread angeordnet sind. Diese Reihenfolge wird jedoch nicht garantiert von anderen Threads beobachtet, da volatile-Zugriffe keine Thread-übergreifende Synchronisation herstellen.

Zusätzlich sind flüchtige Zugriffe nicht atomar (gleichzeitiges Lesen und Schreiben ist ein Datenwettlauf ) und ordnen den Speicher nicht an (nicht-flüchtige Speicherzugriffe können frei um den flüchtigen Zugriff herum neu geordnet werden).

Eine bemerkenswerte Ausnahme ist Visual Studio, wo mit Standardeinstellungen jeder volatile-Schreibvorgang Release-Semantik und jeder volatile-Lesevorgang Acquire-Semantik hat ( Microsoft Docs ), und somit volatiles für die Synchronisation zwischen Threads verwendet werden kann. Die standardmäßigen volatile -Semantiken sind nicht für die Multithread-Programmierung anwendbar, obwohl sie ausreichend sind für z.B. die Kommunikation mit einem std::signal -Handler, der im selben Thread läuft, wenn auf sig_atomic_t -Variablen angewendet. Die Compiler-Option /volatile:iso kann verwendet werden, um das verhaltenskonforme Verhalten mit dem Standard wiederherzustellen, was die Standardeinstellung ist, wenn die Zielplattform ARM ist.

Siehe auch

C-Dokumentation für memory order

Externe Links

1. MOESI-Protokoll
2. x86-TSO: Ein rigoroses und nutzbares Programmiermodell für x86-Multiprozessoren P. Sewell et. al., 2010
3. Eine tutorialhafte Einführung in die ARM- und POWER-Relaxed-Memory-Modelle P. Sewell et al, 2012
4. MESIF: Ein Zwei-Hop-Cache-Kohärenzprotokoll für Punkt-zu-Punkt-Verbindungen J.R. Goodman, H.H.J. Hum, 2009
5. Speichermodelle Russ Cox, 2021