memory_order
|
Definiert in Header
<stdatomic.h>
|
||
|
enum
memory_order
{
|
(seit C11) | |
memory_order
spezifiziert, wie Speicherzugriffe, einschließlich regulärer, nicht-atomarer Speicherzugriffe, um eine atomare Operation herum angeordnet werden sollen. Ohne jegliche Einschränkungen in einem Mehrkernsystem, wenn mehrere Threads gleichzeitig auf mehrere Variablen lesend und schreibend zugreifen, kann ein Thread die Werte in einer anderen Reihenfolge ändern sehen, als die Reihenfolge, in der ein anderer Thread sie geschrieben hat. Tatsächlich kann die scheinbare Reihenfolge der Änderungen sogar zwischen mehreren Leser-Threads variieren. Einige ähnliche Effekte können sogar auf Einprozessorsystemen auftreten, aufgrund von Compiler-Transformationen, die durch das Speichermodell erlaubt sind.
Das Standardverhalten aller atomaren Operationen in der
Sprache
und der Bibliothek bietet
sequenziell konsistente Reihenfolge
(siehe Diskussion unten). Diese Standardeinstellung kann die Leistung beeinträchtigen, aber den atomaren Operationen der Bibliothek kann ein zusätzliches
memory_order
-Argument übergeben werden, um die genauen Einschränkungen anzugeben, die über die Atomarität hinaus vom Compiler und Prozessor für diesen Vorgang erzwungen werden müssen.
Inhaltsverzeichnis |
Konstanten
|
Definiert im Header
<stdatomic.h>
|
|
| Wert | Erklärung |
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
(in C++26 veraltet) |
Ein Ladevorgang mit dieser Speicherreihenfolge führt einen Consume-Vorgang auf dem betroffenen Speicherort 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 eine Acquire-Operation auf dem betroffenen Speicherort 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 eine Release-Operation 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 eine Acquire-Operation als auch eine Release-Operation . 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 eine Acquire-Operation aus, ein Schreibvorgang führt eine Release-Operation aus, und ein Lese-Modifizieren-Schreiben-Vorgang führt sowohl eine Acquire-Operation als auch eine Release-Operation aus, zusätzlich existiert eine einzige Gesamtreihenfolge, in der alle Threads alle Modifikationen in derselben Reihenfolge beobachten (siehe Sequentiell-konsistente Reihenfolge unten). |
|
Dieser Abschnitt ist unvollständig
Grund: happens-before und andere Konzepte, wie in C++, aber Änderungsreihenfolgen und die vier Konsistenzen beibehalten in c/language/atomic |
|
Dieser Abschnitt ist unvollständig
Grund: Bei der obigen Umsetzung nicht vergessen, dass obwohl happens-before in C11 wie veröffentlicht nicht azyklisch war, dies über DR 401 an C++11 angepasst wurde |
Relaxierte Reihenfolge
Atomare Operationen, die mit memory_order_relaxed gekennzeichnet sind, sind keine Synchronisationsoperationen; sie erzwingen keine Reihenfolge zwischen gleichzeitigen Speicherzugriffen. Sie gewährleisten lediglich Atomarität und Konsistenz der Modifikationsreihenfolge.
Zum Beispiel, mit x und y anfänglich null,
// Thread 1:
r1
=
atomic_load_explicit
(
y, memory_order_relaxed
)
;
// A
atomic_store_explicit
(
x, r1, memory_order_relaxed
)
;
// B
// Thread 2:
r2
=
atomic_load_explicit
(
x, memory_order_relaxed
)
;
// C
atomic_store_explicit
(
y,
42
, memory_order_relaxed
)
;
// D
darf
r1
==
r2
==
42
erzeugen, da zwar A
sequenced-before
B in Thread 1 und C
sequenced before
D in Thread 2 ist, nichts aber verhindert, dass D in der Modifikationsreihenfolge von
y
vor A erscheint und B in der Modifikationsreihenfolge von
x
vor C 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 durch Compiler-Neuordnung oder zur Laufzeit.
Typische Verwendung für entspannte Speicherordnung ist das Inkrementieren von Zählern, wie beispielsweise die Referenzcounter, da dies nur Atomarität erfordert, aber keine Reihenfolge oder Synchronisation.
Release-Consume-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_consume gekennzeichnet ist, und der Ladevorgang in Thread B einen durch den Speichervorgang in Thread A geschriebenen Wert liest, dann ist der Speichervorgang in Thread A abhängigkeitsgeordnet vor dem Ladevorgang in Thread B.
Alle Speicherschreibvorgänge (nicht-atomar und relaxed atomar), die happened-before dem atomaren Schreibvorgang aus Sicht von Thread A stehen, werden zu visible side-effects in jenen Operationen in Thread B, in die der Ladevorgang carries dependency , das heißt, sobald der atomare Ladevorgang abgeschlossen ist, ist garantiert, dass jene Operatoren und Funktionen in Thread B, die den aus dem Ladevorgang erhaltenen Wert verwenden, die von Thread A in den Speicher geschriebenen Daten sehen.
Die Synchronisierung wird nur zwischen den Threads hergestellt, die releasing und consuming derselben atomaren Variable sind. 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. ist es dem Compiler untersagt, spekulative Ladevorgänge für die Objekte durchzuführen, die in der Abhängigkeitskette involviert sind).
Typische Anwendungsfälle für diese Reihenfolge betreffen Lesezugriffe auf selten geschriebene nebenläufige Datenstrukturen (Routing-Tabellen, Konfigurationen, Sicherheitsrichtlinien, Firewall-Regeln etc.) und Publisher-Subscriber-Szenarien mit zeigervermittelter Veröffentlichung, das heißt, wenn der Produzent einen Zeiger veröffentlicht, über den der Konsument auf Informationen zugreifen kann: Es ist nicht erforderlich, alles andere, was der Produzent in den Speicher geschrieben hat, für den Konsumenten sichtbar zu machen (was auf schwach geordneten Architekturen eine teure Operation sein kann). Ein Beispiel für ein solches Szenario ist
rcu_dereference
.
Beachten Sie, dass derzeit (2/2015) keine bekannten Produktionscompiler Abhängigkeitsketten verfolgen: consume-Operationen werden zu acquire-Operationen angehoben.
Release-Sequenz
Wenn ein atomarer Wert mit Store-Release geschrieben wird und mehrere andere Threads Lese-Modifizier-Schreib-Operationen auf diesem atomaren Wert ausführen, wird eine "Release-Sequenz" gebildet: Alle Threads, die Lese-Modifizier-Schreib-Operationen auf denselben atomaren Wert ausführen, synchronisieren sich mit dem ersten Thread und untereinander, selbst wenn sie keine
memory_order_release
Semantik besitzen. Dies ermöglicht Single-Producer-Multiple-Consumer-Szenarien, ohne unnötige Synchronisation zwischen einzelnen Consumer-Threads zu erzwingen.
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 waren, werden zu visible side-effects 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. 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 release und acquire auf dieselbe atomare Variable anwenden. 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 Mutexe oder atomare Spinlocks , sind ein Beispiel für Release-Acquire-Synchronisierung: 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.
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,
jede
memory_order_seq_cst
Operation B, die von der atomaren Variable M lädt, beobachtet eines der Folgenden:
- das Ergebnis der letzten Operation A, die M modifizierte und in der einzelnen Gesamtordnung vor B erscheint,
-
ODER, falls es ein solches A gab, kann B das Ergebnis einer Modifikation an M beobachten, die nicht
memory_order_seq_cstist und nicht happen-before A, -
ODER, falls es kein solches A gab, kann B das Ergebnis einer nicht zusammenhängenden Modifikation von M beobachten, die nicht
memory_order_seq_cstist.
Wenn es eine
memory_order_seq_cst
atomic_thread_fence
Operation X gab, die
sequenced-before
B steht, dann beobachtet B eines der Folgenden:
-
die letzte
memory_order_seq_cst-Modifikation von M, die vor X in der gesamten 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, wenn es zwei
memory_order_seq_cst
atomic_thread_fence
s X und Y gibt, und wenn A
sequenced-before
X ist, Y
sequenced-before
B ist, und X im Single Total Order vor Y erscheint, dann beobachtet B entweder:
- die Wirkung von A,
- eine nicht zusammenhängende Modifikation von M, die nach A in der Modifikationsreihenfolge von M erscheint.
Für ein Paar atomarer Modifikationen von M, genannt A und B, tritt B nach A in der Modifikationsreihenfolge von M auf, wenn
-
Es gibt einen
memory_order_seq_cstatomic_thread_fence X, sodass A sequenced-before X ist und X vor B in der Single Total Order erscheint, -
oder es gibt einen
memory_order_seq_cstatomic_thread_fence Y, sodass Y sequenced-before B ist und A vor Y in der Single Total Order erscheint, -
oder es gibt
memory_order_seq_cstatomic_thread_fence s X und Y, sodass A sequenced-before X ist, Y sequenced-before B ist und X vor Y in der Single Total Order erscheint.
Beachten Sie, dass dies bedeutet:
memory_order_seq_cst
gekennzeichnet sind, ins Spiel kommen, geht die sequenzielle Konsistenz verloren,
Sequenzielle Reihenfolge kann in Mehrfachproduzenten-Mehrmalverbraucher-Situationen erforderlich sein, bei denen alle Verbraucher 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.
Beziehung zu volatile
Innerhalb eines Ausführungsstrangs können Zugriffe (Lese- und Schreibvorgänge) durch volatile lvalues nicht über beobachtbare Seiteneffekte (einschließlich anderer volatile-Zugriffe) hinaus umgeordnet werden, die durch einen Sequenzpunkt im selben Strang getrennt sind. Diese Reihenfolge ist jedoch nicht garantiert für andere Stränge beobachtbar, 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 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 volatile-Variablen für die Synchronisation zwischen Threads verwendet werden können. Die Standard-
volatile
-Semantik ist nicht für die Multithread-Programmierung geeignet, obwohl sie ausreicht für z.B. die Kommunikation mit einem
Signal-Handler
, der im selben Thread läuft, wenn sie auf
sig_atomic_t
-Variablen angewendet wird. Die Compiler-Option
/volatile:iso
kann verwendet werden, um das standardkonforme Verhalten wiederherzustellen, was die Standardeinstellung ist, wenn die Zielplattform ARM ist.
Beispiele
|
Dieser Abschnitt ist unvollständig
Grund: Kein Beispiel |
Referenzen
- C23-Standard (ISO/IEC 9899:2024):
-
- 7.17.1/4 memory_order (S.: TBD)
-
- 7.17.3 Reihenfolge und Konsistenz (S.: TBD)
- C17-Standard (ISO/IEC 9899:2018):
-
- 7.17.1/4 memory_order (S: 200)
-
- 7.17.3 Ordnung und Konsistenz (S: 201-203)
- C11-Standard (ISO/IEC 9899:2011):
-
- 7.17.1/4 memory_order (S. 273)
-
- 7.17.3 Ordnung und Konsistenz (S. 275-277)
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 |
|
Dieser Abschnitt ist unvollständig
Grund: Wir sollten gute Referenzen zu QPI, MOESI und eventuell Dragon finden. |