Namespaces
Variants

memory_order

From cppreference.net
Definiert in Header <stdatomic.h>
enum memory_order

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

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

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

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_cst atomic_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_cst atomic_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_cst atomic_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:

1) sobald atomare Operationen, die nicht mit memory_order_seq_cst gekennzeichnet sind, ins Spiel kommen, geht die sequenzielle Konsistenz verloren,
2) die sequenziell-konsistenten Fences stellen nur eine totale Ordnung für die Fences selbst her, nicht für die atomaren Operationen im allgemeinen Fall ( sequenced-before ist keine threadsübergreifende Beziehung, im Gegensatz zu happens-before ).

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

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