Namensräume
Varianten
Aktionen

std::memory_order

Von cppreference.com
< cpp‎ | atomic
 
 
Bibliothek für nebenläufige Programmierung
Threads
(C++11)
(C++20)
this_thread Namespace
(C++11)
(C++11)
(C++11)
Kooperatives Beenden
Gegenseitiger Ausschluss
(C++11)
Allgemeines Sperrungsmanagement
(C++11)
(C++11)
(C++11)
(C++11)
(C++11)
Bedingungsvariablen
(C++11)
Semaphoren
Latches und Barriers
(C++20)
(C++20)
Futures
(C++11)
(C++11)
(C++11)
(C++11)
Sichere Wiederherstellung
(C++26)
Hazard Pointer
Atomare Typen
(C++11)
(C++20)
Initialisierung von atomaren Typen
(C++11)(veraltet in C++20)
(C++11)(veraltet in C++20)
Speicherordnung
memory_order
(C++11)
(C++11)(deprecated in C++26)
Freie Funktionen für atomare Operationen
Freie Funktionen für atomare Flags
 
Definiert in 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 : /* unspecified */

{
    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 geordnet werden sollen. Ohne Einschränkungen auf einem Mehrkernsystem, wenn mehrere Threads gleichzeitig auf mehrere Variablen lesen und schreiben, kann ein Thread die Änderungen der Werte in einer anderen Reihenfolge beobachten als eine andere Thread sie geschrieben hat. Tatsächlich kann die scheinbare Reihenfolge der Änderungen sogar zwischen mehreren Lese-Threads variieren. Ähnliche Effekte können auch auf Uniprocessor-Systemen aufgrund von Compiler-Transformationen auftreten, die durch das Speichermodell erlaubt sind.

Das Standardverhalten aller atomaren Operationen in der Bibliothek bietet eine sequenziell konsistente Ordnung (siehe Diskussion unten). Dieses Standardverhalten 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 festzulegen, die der Compiler und der Prozessor über die Atomarität hinaus für diese Operation durchsetzen müssen.

Inhalt

[edit] Konstanten

Definiert in Header <atomic>
Name Erklärung
memory_order_relaxed Relaxed operation: Es werden keine Synchronisations- oder Ordnungsbeschränkungen für andere Lese- oder Schreibvorgänge auferlegt, nur die Atomarität dieser Operation ist garantiert (siehe Relaxed ordering unten).
memory_order_consume
(veraltet in C++26)
Eine Ladeoperation mit dieser Speicherordnung führt eine Consume-Operation auf dem betroffenen Speicherort aus: Keine Lese- oder Schreibvorgänge im aktuellen Thread, die vom Wert des aktuell geladenen Wertes abhängen, können vor diesem Ladevorgang neu geordnet werden. Schreibvorgänge auf abhängige Variablen in anderen Threads, die dieselbe atomare Variable freigeben, sind im aktuellen Thread sichtbar. Auf den meisten Plattformen betrifft dies nur Compiler-Optimierungen (siehe Release-Consume ordering unten).
memory_order_acquire Eine Ladeoperation mit dieser Speicherordnung führt die Acquire-Operation auf dem betroffenen Speicherort aus: Keine Lese- oder Schreibvorgänge im aktuellen Thread können vor diesem Ladevorgang neu geordnet werden. Alle Schreibvorgänge in anderen Threads, die dieselbe atomare Variable freigeben, sind im aktuellen Thread sichtbar (siehe Release-Acquire ordering unten).
memory_order_release Eine Speicheroperation mit dieser Speicherordnung führt die Release-Operation aus: Keine Lese- oder Schreibvorgänge im aktuellen Thread können nach diesem Speicherzugriff neu geordnet werden. Alle Schreibvorgänge im aktuellen Thread sind in anderen Threads sichtbar, die dieselbe atomare Variable übernehmen (siehe Release-Acquire ordering unten), und Schreibvorgänge, die eine Abhängigkeit in die atomare Variable tragen, werden in anderen Threads sichtbar, die dieselbe atomare Variable verbrauchen (siehe Release-Consume ordering unten).
memory_order_acq_rel Eine Lese-Modifizierungs-Schreib-Operation mit dieser Speicherordnung ist sowohl eine Acquire-Operation als auch eine Release-Operation. Keine Speicherlese- oder Schreibvorgänge im aktuellen Thread können vor dem Laden oder nach dem Speichern neu geordnet werden. Alle Schreibvorgänge in anderen Threads, die dieselbe atomare Variable freigeben, sind vor der Modifizierung sichtbar und die Modifizierung ist in anderen Threads sichtbar, die dieselbe atomare Variable übernehmen.
memory_order_seq_cst Eine Ladeoperation mit dieser Speicherordnung führt eine Acquire-Operation aus, ein Speichern führt eine Release-Operation aus, und eine Lese-Modifizierungs-Schreib-Operation führt sowohl eine Acquire-Operation als auch eine Release-Operation aus, plus eine einzelne totale Reihenfolge, in der alle Threads alle Modifikationen in derselben Reihenfolge beobachten (siehe Sequentially-consistent ordering unten).

[edit] Formale Beschreibung

Inter-Thread-Synchronisation und Speicherordnung bestimmen, wie Auswertungen und Nebeneffekte von Ausdrücken zwischen verschiedenen Ausführungs-Threads geordnet werden. Sie werden in den folgenden Begriffen definiert

[edit] Sequenced-before

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

Carries dependency

Innerhalb desselben Threads kann die Auswertung A, die sequenced-before die Auswertung B ist, auch eine Abhängigkeit in B tragen (d. h. B hängt von A ab), wenn eine der folgenden Bedingungen 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 Operatoren &&, ||, ?:, oder , ist.
2) A schreibt in ein Skalarobjekt M, B liest aus M.
3) A trägt eine Abhängigkeit in eine andere Auswertung X, und X trägt eine Abhängigkeit in B.
(bis C++26)

[edit] Modification order

Alle Modifikationen einer bestimmten atomaren Variablen erfolgen in einer totalen Reihenfolge, 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 die Auswertung A, die eine atomare Variable M modifiziert (ein Schreibvorgang), happens-before der Auswertung B, die M modifiziert, dann erscheint A früher als B in der modification order von M.
2) Read-read coherence: Wenn eine Wertberechnung A einer atomaren Variable M (ein Lesevorgang) happens-before einer Wertberechnung B auf M, und wenn der Wert von A aus einem Schreibvorgang X auf M stammt, dann ist der Wert von B entweder der von X gespeicherte Wert oder der Wert eines Nebeneffekts Y auf M, der später in der modification order von M als X erscheint.
3) Read-write coherence: Wenn eine Wertberechnung A einer atomaren Variable 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 früher in der modification order von M als B erscheint.
4) Write-read coherence: Wenn ein Nebeneffekt (ein Schreibvorgang) X auf einem atomaren Objekt M happens-before einer Wertberechnung (einem Lesevorgang) B von M, dann bezieht B seinen Wert aus X oder aus einem Nebeneffekt Y, der in der modification order von M auf X folgt.

[edit] Release sequence

Nachdem eine Release-Operation A auf einem atomaren Objekt M ausgeführt wurde, ist die längste zusammenhängende Teilsequenz der modification order von M, die besteht aus

1) Schreibvorgängen, die vom selben Thread ausgeführt wurden, der A ausgeführt hat.
(bis C++20)
2) Atomaren Lese-Modifizierungs-Schreib-Operationen, die an M von einem beliebigen Thread vorgenommen wurden.

wird als Release Sequence, die von A angeführt wird, bezeichnet.

[edit] Synchronizes with

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

Außerdem können einige Bibliotheksaufrufe definiert sein, um sich mit anderen Bibliotheksaufrufen auf anderen Threads zu synchronisieren.

Dependency-ordered before

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

1) A führt eine Release-Operation auf einem atomaren Objekt M aus, und in einem anderen Thread führt B eine Consume-Operation auf demselben atomaren Objekt M aus, und B liest einen Wert, der von irgendeinem Teil der Release-Sequenz, die von(bis C++20) A angeführt wird, geschrieben wurde.
2) A ist dependency-ordered before X und X trägt eine Abhängigkeit in B.
(bis C++26)

[edit] Inter-thread happens-before

Zwischen Threads inter-thread happens before die Auswertung A die Auswertung B, wenn eine der folgenden Bedingungen zutrifft

1) A synchronisiert sich mit B.
2) A ist dependency-ordered before B.
3) A synchronisiert sich mit 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 happens-before die Auswertung A die Auswertung B, wenn eine der folgenden Bedingungen zutrifft

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

Die Implementierung ist verpflichtet sicherzustellen, dass die happens-before-Beziehung azyklisch ist, indem bei Bedarf zusätzliche Synchronisationen eingeführt werden (dies ist nur erforderlich, wenn eine Consume-Operation beteiligt ist, siehe Batty et al).

Wenn eine Auswertung eine Speicherposition modifiziert und die andere dieselbe Speicherposition liest oder modifiziert, und wenn mindestens eine der Auswertungen keine atomare Operation 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 simply happens-before die Auswertung A die Auswertung B, wenn eine der folgenden Bedingungen zutrifft

1) A ist sequenced-before B.
2) A synchronisiert sich mit 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 gleich.

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

Happens-before

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

1) A ist sequenced-before B.
2) A synchronisiert sich mit B.
3) A happens-before X, und X happens-before B.
(seit C++26)

[edit] Strongly happens-before

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

1) A ist sequenced-before B.
2) A synchronisiert sich mit 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 sequenziell konsistente atomare Operationen.
3) A ist sequenced-before X, X simply(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 ausgedrückt, wenn A strongly happens-before B, dann erscheint A in allen Kontexten vor B ausgewertet zu werden.

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

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

[edit] Sichtbare Nebeneffekte

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

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

Wenn der 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, auf die B nicht happens-before ist, bekannt als die sichtbare Sequenz von Nebeneffekten (der Wert von M, bestimmt durch B, ist der Wert, der von einem dieser Nebeneffekte gespeichert wurde).

Hinweis: Inter-Thread-Synchronisation reduziert sich auf die Verhinderung von Datenrennen (durch Etablierung von Happens-before-Beziehungen) und die Bestimmung, welche Nebeneffekte unter welchen Bedingungen sichtbar werden.

[edit] Consume operation

Eine atomare Ladeoperation mit memory_order_consume oder stärker ist eine Consume-Operation. Beachten Sie, dass std::atomic_thread_fence stärkere Synchronisationsanforderungen stellt als eine Consume-Operation.

[edit] Acquire operation

Eine atomare Ladeoperation mit memory_order_acquire oder stärker ist eine Acquire-Operation. Die Operation lock() auf einer Mutex ist ebenfalls eine Acquire-Operation. Beachten Sie, dass std::atomic_thread_fence stärkere Synchronisationsanforderungen stellt als eine Acquire-Operation.

[edit] Release operation

Eine atomare Speicheroperation mit memory_order_release oder stärker ist eine Release-Operation. Die Operation unlock() auf einer Mutex ist ebenfalls eine Release-Operation. Beachten Sie, dass std::atomic_thread_fence stärkere Synchronisationsanforderungen stellt als eine Release-Operation.

[edit] Erklärung

[edit] Relaxed ordering

Atomare Operationen mit dem Tag memory_order_relaxed sind keine Synchronisationsoperationen; sie erzwingen keine Ordnung zwischen gleichzeitigen Speicherzugriffen. Sie garantieren nur Atomarität und Modifikationsreihenfolge-Konsistenz.

Zum Beispiel, mit x und y initial 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

darf r1 == r2 == 42 ergeben, da, obwohl A sequenced-before B innerhalb von Thread 1 und C sequenced-before D innerhalb von Thread 2, nichts D daran hindert, vor A in der modification order von y zu erscheinen, und B daran hindert, vor C in der modification order von x zu erscheinen. Der Nebeneffekt von D auf y könnte für den Ladevorgang A in Thread 1 sichtbar sein, während der Nebeneffekt 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-Reordering oder zur Laufzeit.

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

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

darf r1 == r2 == 42 nicht ergeben, da der Schreibvorgang von 42 auf y nur möglich ist, wenn der Schreibvorgang auf x 42 speichert, was zirkulär von dem Schreibvorgang auf y abhängt, der 42 speichert. Beachten Sie, dass bis C++14 dies technisch von der Spezifikation erlaubt war, aber nicht für Implementierer empfohlen wurde.

(seit C++14)

Die typische Verwendung für entspannte Speicherordnung ist das Inkrementieren von Zählern, wie z. B. den Referenzzählern von std::shared_ptr, da dies nur Atomarität, aber keine Ordnung oder Synchronisation erfordert (beachten Sie, 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

[edit] Release-Acquire ordering

Wenn ein atomarer Schreibvorgang in Thread A mit memory_order_release getaggt ist, ein atomarer Ladevorgang in Thread B von derselben Variablen mit memory_order_acquire getaggt ist und der Ladevorgang in Thread B einen Wert liest, der von dem Schreibvorgang in Thread A geschrieben wurde, dann synchronisiert sich der Schreibvorgang in Thread A mit dem Ladevorgang in Thread B.

Alle Speicher Schreibvorgänge (einschließlich nicht-atomare und relaxierte atomare), die aus Sicht von Thread A happened-before dem atomaren Schreibvorgang stattfanden, werden zu sichtbaren Nebeneffekten in Thread B. Das heißt, sobald der atomare Ladevorgang abgeschlossen ist, ist garantiert, dass Thread B alles sieht, was Thread A in den Speicher geschrieben hat. Dieses Versprechen gilt nur, wenn B tatsächlich den Wert zurückgibt, den A gespeichert hat, oder einen Wert aus späterer Zeit in der Release-Sequenz.

Die Synchronisation wird nur zwischen den Threads etabliert, die dieselbe atomare Variable freigeben und übernehmen. Andere Threads können eine andere Reihenfolge von Speicherzugriffen sehen als einer oder beide der synchronisierten Threads.

Auf stark geordneten Systemen – x86, SPARC TSO, IBM Mainframe usw. – ist die Release-Acquire-Ordnung für die meisten Operationen automatisch. Es werden keine zusätzlichen CPU-Befehle für diesen Synchronisationsmodus ausgegeben; nur bestimmte Compiler-Optimierungen werden beeinflusst (z. B. dem Compiler ist es untersagt, nicht-atomare Schreibvorgänge über den atomaren Schreibvorgang-Release hinauszubewegen oder nicht-atomare Ladevorgänge früher als den atomaren Ladevorgang-Acquire durchzuführen). Auf schwach geordneten Systemen (ARM, Itanium, PowerPC) werden spezielle Lade- oder Speicher-Fence-Befehle verwendet.

Mutual Exclusion Locks, wie std::mutex oder atomic spinlock, sind ein Beispiel für Release-Acquire-Synchronisation: Wenn der Lock von Thread A freigegeben und von Thread B übernommen wird, müssen alle Ereignisse im kritischen Abschnitt (vor der Freigabe) im Kontext von Thread A für Thread B (nach der Übernahme), der denselben kritischen Abschnitt ausführt, sichtbar sein.

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

[edit] Release-Consume ordering

Wenn ein atomarer Schreibvorgang in Thread A mit memory_order_release getaggt ist, ein atomarer Ladevorgang in Thread B von derselben Variablen mit memory_order_consume getaggt ist und der Ladevorgang in Thread B einen Wert liest, der von dem Schreibvorgang in Thread A geschrieben wurde, dann ist der Schreibvorgang in Thread A dependency-ordered before dem Ladevorgang in Thread B.

Alle Speicher Schreibvorgänge (nicht-atomare und relaxierte atomare), die aus Sicht von Thread A happened-before dem atomaren Schreibvorgang stattfanden, werden zu sichtbaren Nebeneffekten innerhalb der Operationen in Thread B, in die der Ladevorgang eine Abhängigkeit trägt. Das heißt, sobald die atomare Ladeoperation abgeschlossen ist, sind die Operatoren und Funktionen in Thread B, die den erhaltenen Wert verwenden, garantiert zu sehen, was Thread A in den Speicher geschrieben hat.

Die Synchronisation wird nur zwischen den Threads etabliert, die dieselbe atomare Variable freigeben und verbrauchen. 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, es werden keine zusätzlichen CPU-Befehle für diesen Synchronisationsmodus ausgegeben, nur bestimmte Compiler-Optimierungen sind betroffen (z. B. dem Compiler ist es untersagt, spekulative Ladevorgänge auf Objekten durchzuführen, die an der Abhängigkeitskette beteiligt sind).

Typische Anwendungsfälle für diese Ordnung umfassen den schreibgeschützten Zugriff auf selten geschriebene gleichzeitige Datenstrukturen (Routing-Tabellen, Konfigurationen, Sicherheitsrichtlinien, Firewall-Regeln usw.) und Publisher-Subscriber-Situationen mit Pointer-basierter Publikation, d. h. wenn der Produzent einen Pointer veröffentlicht, über den der Konsument auf Informationen zugreifen kann: Es ist nicht notwendig, alles andere, was der Produzent in den Speicher geschrieben hat, für den Konsumenten sichtbar zu machen (was auf schwach geordneten Architekturen eine kostspielige Operation sein kann). Ein Beispiel für ein solches Szenario ist rcu_dereference.

Siehe auch std::kill_dependency und [[carries_dependency]] für feingranulare Abhängigkeitskettenkontrolle.

Beachten Sie, dass derzeit (2/2015) keine bekannten Produktionscompiler Abhängigkeitsketten verfolgen: Consume-Operationen werden zu Acquire-Operationen hochgestuft.

(bis C++26)

Die Spezifikation der Release-Consume-Ordnung wird überarbeitet, und die Verwendung von memory_order_consume wird vorübergehend abgeraten.

(seit C++17)
(bis C++26)

Release-Consume-Ordnung hat die gleiche Auswirkung wie Release-Acquire-Ordnung und ist veraltet.

(seit C++26)

Dieses Beispiel demonstriert Dependency-Ordered-Synchronisation für Pointer-basierte Publikation: Die Integer-Daten sind nicht durch eine Datenabhängigkeitsbeziehung mit dem Pointer auf den String verbunden, daher ist ihr Wert im Konsumenten 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();
}


[edit] Sequentially-consistent ordering

Atomare Operationen mit dem Tag memory_order_seq_cst ordnen den Speicher nicht nur so wie die Release/Acquire-Ordnung (alles, was in einem Thread happened-before einem Speicherzugriff stattfand, wird zu einem sichtbaren Nebeneffekt in dem Thread, der einen Ladevorgang durchgeführt hat), sondern sie etablieren auch eine einzelne totale Modifikationsreihenfolge aller so getaggten atomaren Operationen.

Formal,

jede memory_order_seq_cst-Operation B, die von der atomaren Variable M liest, beobachtet eine der folgenden

  • das Ergebnis der letzten Operation A, die M modifiziert hat und die vor B in der einzelnen totalen Reihenfolge erscheint,
  • ODER, wenn es ein solches A gab, kann B das Ergebnis einer Modifikation an M beobachten, die nicht memory_order_seq_cst ist und nicht happens-before A,
  • ODER, wenn es kein solches A gab, kann B das Ergebnis einer nicht zusammenhängenden Modifikation von M beobachten, die später in der modification order von M liegt.

Wenn es eine memory_order_seq_cst std::atomic_thread_fence-Operation X gab, die sequenced-before B ist, dann beobachtet B eine der folgenden

  • die letzte memory_order_seq_cst-Modifikation von M, die vor X in der einzelnen totalen Reihenfolge erscheint,
  • eine nicht zusammenhängende Modifikation von M, die später in der modification order von M erscheint.

Für ein Paar atomarer Operationen an M namens A und B, wobei A schreibt und B den Wert von M liest, wenn es zwei memory_order_seq_cst std::atomic_thread_fences X und Y gibt, und wenn A sequenced-before X ist, Y sequenced-before B ist und X vor Y in der Single Total Order erscheint, dann beobachtet B entweder

  • den Effekt von A,
  • eine nicht zusammenhängende Modifikation von M, die nach A in der modification order von M erscheint.

Für ein Paar atomarer Modifikationen von M, die A und B genannt werden, tritt B in der Modifikationsreihenfolge von M nach A auf, wenn

  • eine memory_order_seq_cst std::atomic_thread_fence X existiert, so dass A sequenced-before X ist und X in der Single Total Order vor B auftritt,
  • oder eine memory_order_seq_cst std::atomic_thread_fence Y existiert, so dass Y sequenced-before B ist und A vor Y in der Single Total Order auftritt,
  • oder memory_order_seq_cst std::atomic_thread_fences X und Y existieren, so dass A sequenced-before X ist, Y sequenced-before B ist und X vor Y in der Single Total Order auftritt.

Dies bedeutet, dass

1) sobald atomare Operationen, die nicht mit memory_order_seq_cst getaggt sind, ins Spiel kommen, die sequentielle Konsistenz verloren geht,
2) die sequenziell konsistenten Fences nur eine Gesamtordnung für die Fences selbst herstellen, nicht für die atomaren Operationen im allgemeinen Fall (sequenced-before ist keine Thread-übergreifende Beziehung, im Gegensatz zu happens-before).
(bis C++20)
Formal,

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 Wert, der von einer atomaren Modifikation X gespeichert wurde, X geht B in der modification order voraus und A und B sind nicht dieselbe atomare Lese-Modifikations-Schreib-Operation,
4) A ist coherence-ordered-before X und X ist coherence-ordered-before B.

Es gibt eine einzige Gesamtordnung S über alle memory_order_seq_cst Operationen, einschließlich Fences, die die folgenden Einschränkungen erfüllt:

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 einem memory_order_seq_cst Fence Y happens-before, dann geht A Y in S voraus,
c) wenn ein memory_order_seq_cst Fence X A happens-before 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 A happens-before und B einem memory_order_seq_cst Fence Y happens-before, dann geht X Y in S voraus.

Die formale Definition stellt sicher, dass

1) die einzelne Gesamtordnung konsistent mit der modification order jedes atomaren Objekts ist,
2) ein memory_order_seq_cst Load seinen Wert entweder von der letzten memory_order_seq_cst Modifikation oder von einer Nicht-memory_order_seq_cst Modifikation erhält, die nicht den vorhergehenden memory_order_seq_cst Modifikationen happens-before.

Die einzelne Gesamtordnung ist möglicherweise nicht konsistent mit happens-before. Dies ermöglicht eine effizientere Implementierung von memory_order_acquire und memory_order_release auf einigen CPUs. Sie kann überraschende Ergebnisse liefern, wenn memory_order_acquire und memory_order_release mit memory_order_seq_cst gemischt werden.

Zum Beispiel, mit x und y, die anfänglich Null sind,

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

ist es erlaubt, r1 == 1 && r2 == 3 && r3 == 0 zu produzieren, wobei A C happens-before, aber C in der einzelnen Gesamtordnung C-E-F-A von memory_order_seq_cst (siehe Lahav et al) vor A auftritt.

Beachten Sie, dass

1) sobald atomare Operationen, die nicht mit memory_order_seq_cst getaggt sind, ins Spiel kommen, die Garantie der sequenziellen Konsistenz für das Programm verloren geht,
2) in vielen Fällen atomare Operationen mit memory_order_seq_cst in Bezug auf andere atomare Operationen desselben Threads neu angeordnet werden können.
(seit C++20)

Sequentielle Ordnung kann für Multi-Producer-Multi-Consumer-Situationen notwendig sein, in denen alle Consumer die Aktionen aller Producer in derselben Reihenfolge beobachten müssen.

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

Dieses Beispiel demonstriert eine Situation, in der sequentielle Ordnung notwendig ist. Jede andere Ordnung kann den Assert auslösen, da es möglich wäre, dass die Threads c und d Änderungen an den Atomen x und y in umgekehrter 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
}

[bearbeiten] Beziehung mit volatile

Innerhalb eines Ausführungsthreads dürfen Zugriffe (Lese- und Schreibvorgänge) über volatile glvalues nicht über beobachtbare Nebeneffekte (einschließlich anderer volatiler Zugriffe) hinaus neu angeordnet werden, die innerhalb desselben Threads sequenced-before oder sequenced-after sind, aber diese Ordnung muss nicht notwendigerweise von einem anderen Thread beobachtet werden, da volatile Zugriffe keine Thread-übergreifende Synchronisation herstellen.

Darüber hinaus sind volatile Zugriffe nicht atomar (gleichzeitiges Lesen und Schreiben ist ein data race) und ordnen keinen Speicher (nicht-volatile Speicherzugriffe können frei um den volatilen Zugriff herum neu angeordnet werden).

Eine bemerkenswerte Ausnahme ist Visual Studio, wo standardmäßig jeder volatile Schreibvorgang Release-Semantik und jeder volatile Lesevorgang Acquire-Semantik hat (Microsoft Docs), und somit volatile Variablen für die Thread-übergreifende Synchronisation verwendet werden können. Standard volatile Semantik ist für die Multi-Thread-Programmierung nicht anwendbar, obwohl sie z. B. für die Kommunikation mit einem std::signal Handler, der im selben Thread ausgeführt wird, bei Anwendung auf sig_atomic_t Variablen ausreichend ist.

[bearbeiten] Siehe auch

C-Dokumentation für memory_order

[bearbeiten] Externe Links

1.  MOESI-Protokoll
2.  x86-TSO: A Rigorous and Usable Programmer’s Model for x86 Multiprocessors P. Sewell et al., 2010
3.  A Tutorial Introduction to the ARM and POWER Relaxed Memory Models P. Sewell et al., 2012
4.  MESIF: A Two-Hop Cache Coherency Protocol for Point-to-Point Interconnects J.R. Goodman, H.H.J. Hum, 2009
5.  Memory Models Russ Cox, 2021