memory_order
| Definiert im Header <stdatomic.h> |
||
| enum memory_order { |
(seit C11) | |
memory_order gibt an, wie Speicherzugriffe, einschließlich regulärer, nicht-atomarer Speicherzugriffe, um eine atomare Operation herum geordnet werden sollen. Ohne Beschränkungen in einem Multi-Core-System, wenn mehrere Threads gleichzeitig auf mehrere Variablen lesen und schreiben, kann ein Thread die Änderungen an den Werten in einer anderen Reihenfolge beobachten, als ein anderer Thread sie geschrieben hat. Tatsächlich kann die scheinbare Reihenfolge der Änderungen sogar zwischen mehreren Leser-Threads unterschiedlich sein. Ähnliche Effekte können auch auf Uniprozessor-Systemen aufgrund von Compiler-Transformationen auftreten, die durch das Speichermodell erlaubt sind.
Das Standardverhalten aller atomaren Operationen in der Sprache und der Bibliothek sorgt für eine sequenziell konsistente Ordnung (siehe Diskussion unten). Dieser Standard 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 der Compiler und der Prozessor über die Atomarität hinaus für diese Operation erzwingen müssen.
Inhalt |
[bearbeiten] Konstanten
| Definiert im Header
<stdatomic.h> | |
| Wert | Erklärung |
memory_order_relaxed
|
Entspannte Operation: Es gibt keine Synchronisations- oder Ordnungsbeschränkungen für andere Lese- oder Schreibvorgänge, nur die Atomarität dieser Operation ist garantiert (siehe Entspannte Ordnung unten). |
memory_order_consume(veraltet in C++26) |
Eine Ladeoperation mit dieser Speicherordnung führt eine Consume-Operation auf der betroffenen Speicherstelle aus: Keine Lese- oder Schreibvorgänge im aktuellen Thread, die von dem aktuell geladenen Wert abhängen, können vor diesem Ladevorgang neu geordnet werden. Schreibvorgänge auf datenabhä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-Ordnung unten). |
memory_order_acquire
|
Eine Ladeoperation mit dieser Speicherordnung führt die Acquire-Operation auf der betroffenen Speicherstelle 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-Ordnung 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 Speichervorgang neu geordnet werden. Alle Schreibvorgänge im aktuellen Thread sind in anderen Threads sichtbar, die dieselbe atomare Variable erwerben (siehe Release-Acquire-Ordnung 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-Ordnung 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 Ladevorgang oder nach dem Speichervorgang 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 erwerben. |
memory_order_seq_cst
|
Eine Ladeoperation mit dieser Speicherordnung führt eine Acquire-Operation aus, ein Speichervorgang eine Release-Operation und eine Lese-Modifizierungs-Schreib-Operation beides, eine Acquire-Operation und eine Release-Operation, und es existiert eine einzige totale Ordnung, in der alle Threads alle Modifikationen in derselben Reihenfolge beobachten (siehe Sequenziell konsistente Ordnung unten). |
| Dieser Abschnitt ist unvollständig Grund: Happens-before und andere Konzepte, wie in C++, aber Beibehalten von Modifikationsordnungen und den vier Konsistenzen in c/language/atomic |
| Dieser Abschnitt ist unvollständig Grund: Beim oben genannten nicht vergessen, dass, obwohl Happens-before in C11 nicht azyklisch war, dies durch DR 401 zur Übereinstimmung mit C++11 aktualisiert wurde |
[bearbeiten] Entspannte Ordnung
Atomare Operationen mit der Kennzeichnung memory_order_relaxed sind keine Synchronisationsoperationen; sie legen keine Ordnung zwischen gleichzeitigen Speicherzugriffen fest. Sie garantieren nur Atomarität und Konsistenz der Modifikationsordnung.
Zum Beispiel, mit x und y, die anfangs Null sind,
// 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 ist erlaubt, r1 == r2 == 42 zu erzeugen, da, obwohl A in Thread 1 sequenziert-vor B liegt und C in Thread 2 sequenziert-vor D liegt, nichts D davon abhält, vor A in der Modifikationsordnung von y zu erscheinen, und B davon abhält, vor C in der Modifikationsordnung 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 in Thread 2 vor C abgeschlossen wird, entweder aufgrund von Compiler-Neuordnung oder zur Laufzeit.
Typische Anwendung für entspannte Speicherordnung ist das Inkrementieren von Zählern, wie z. B. Referenzzählern, da dies nur Atomarität erfordert, aber keine Ordnung oder Synchronisation.
[bearbeiten] Release-Consume-Ordnung
Wenn ein atomarer Speichervorgang in Thread A als memory_order_release gekennzeichnet ist, ein atomarer Ladevorgang in Thread B von derselben Variablen als memory_order_consume gekennzeichnet ist und der Ladevorgang in Thread B einen von dem Speichervorgang in Thread A geschriebenen Wert liest, dann ist der Speichervorgang in Thread A abhängigkeitsgeordnet vor dem Ladevorgang in Thread B.
Alle Speicherbeschreibungen (nicht-atomare und entspannte atomare) die aus Sicht von Thread A vor dem atomaren Speichervorgang happened-before, werden zu sichtbaren Nebeneffekten innerhalb der Operationen in Thread B, in die der Ladevorgang eine Abhängigkeit trägt, d. h., sobald der atomare Ladevorgang abgeschlossen ist, sind diese Operatoren und Funktionen in Thread B, die den aus dem Ladevorgang erhaltenen Wert verwenden, garantiert, das zu sehen, was Thread A in den Speicher geschrieben hat.
Die Synchronisation wird nur zwischen den Threads hergestellt, die dieselbe atomare Variable freigeben und konsumieren. Andere Threads können eine andere Reihenfolge von Speicherzugriffen sehen als einer oder beide der synchronisierten Threads.
Auf den meisten Mainstream-CPUs außer DEC Alpha ist die Abhängigkeitsordnung automatisch, es werden keine zusätzlichen CPU-Instruktionen für diesen Synchronisationsmodus ausgegeben, nur bestimmte Compiler-Optimierungen sind betroffen (z. B. ist der Compiler daran gehindert, spekulative Ladevorgänge an den Objekten durchzuführen, die in der Abhängigkeitskette beteiligt sind).
Typische Anwendungsfälle für diese Ordnung umfassen den Lesezugriff auf selten geschriebene nebenläufige Datenstrukturen (Routing-Tabellen, Konfigurationen, Sicherheitsrichtlinien, Firewall-Regeln usw.) und Publisher-Subscriber-Situationen mit zeigervermittelter Veröffentlichung, d. h. wenn der Produzent einen Zeiger 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 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 hochgestuft.
[bearbeiten] Release-Sequenz
Wenn ein atomares Objekt mit Store-Release versehen ist und mehrere andere Threads Lese-Modifizierungs-Schreib-Operationen auf diesem atomaren Objekt durchführen, wird eine "Release-Sequenz" gebildet: Alle Threads, die die Lese-Modifizierungs-Schreib-Operationen auf dasselbe atomare Objekt ausführen, synchronisieren sich mit dem ersten Thread und untereinander, auch wenn sie keine memory_order_release-Semantik haben. Dies ermöglicht Situationen mit einem Produzenten und mehreren Konsumenten, ohne unnötige Synchronisation zwischen einzelnen Konsumenten-Threads zu erzwingen.
[bearbeiten] Release-Acquire-Ordnung
Wenn ein atomarer Speichervorgang in Thread A als memory_order_release gekennzeichnet ist, ein atomarer Ladevorgang in Thread B von derselben Variablen als memory_order_acquire gekennzeichnet ist und der Ladevorgang in Thread B einen von dem Speichervorgang in Thread A geschriebenen Wert liest, dann synchronisiert sich der Speichervorgang in Thread A mit dem Ladevorgang in Thread B.
Alle Speicherbeschreibungen (einschließlich nicht-atomarer und entspannter atomarer), die aus Sicht von Thread A vor dem atomaren Speichervorgang happened-before, werden zu sichtbaren Nebeneffekten in Thread B. Das heißt, sobald der atomare Ladevorgang abgeschlossen ist, sind in Thread B garantiert alle von Thread A in den Speicher geschriebenen Werte sichtbar. Dieses Versprechen gilt nur, wenn B tatsächlich den von A gespeicherten Wert oder einen Wert aus einer späteren Position in der Release-Sequenz zurückgibt.
Die Synchronisation wird nur zwischen den Threads hergestellt, die dieselbe atomare Variable freigeben und erwerben. 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 Mehrheit der Operationen automatisch. Es werden keine zusätzlichen CPU-Instruktionen für diesen Synchronisationsmodus ausgegeben; nur bestimmte Compiler-Optimierungen sind betroffen (z. B. ist der Compiler daran gehindert, nicht-atomare Schreibvorgänge über den atomaren Store-Release hinaus 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-Instruktionen verwendet.
Mutual-Exclusion-Sperren, wie z. B. Mutexes oder atomare Spinlocks, sind ein Beispiel für Release-Acquire-Synchronisation: Wenn die Sperre von Thread A freigegeben und von Thread B erworben wird, müssen alle im kritischen Abschnitt (vor der Freigabe) im Kontext von Thread A stattgefundenen Ereignisse für Thread B (nach dem Erwerb) sichtbar sein, der denselben kritischen Abschnitt ausführt.
[bearbeiten] Sequenziell konsistente Ordnung
Atomare Operationen, die mit memory_order_seq_cst gekennzeichnet sind, ordnen den Speicher nicht nur so wie die Release/Acquire-Ordnung (alles, was vor einem Speichervorgang in einem Thread happened-before, wird zu einem sichtbaren Nebeneffekt in dem Thread, der einen Ladevorgang durchgeführt hat), sondern etablieren auch eine einzige totale Modifikationsordnung aller so gekennzeichneten atomaren Operationen.
Formal,
jede memory_order_seq_cst-Operation B, die von der atomaren Variablen M lädt, beobachtet eine der folgenden
- das Ergebnis der letzten Operation A, die M modifiziert hat und die vor B in der einzigen totalen Ordnung erscheint,
- ODER, wenn es ein solches A gab, kann B das Ergebnis einer Modifikation an M beobachten, die nicht
memory_order_seq_cstist und nicht happened-before A, - ODER, wenn 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 sequenziert-vor B liegt, dann beobachtet B eine der folgenden
- die letzte
memory_order_seq_cst-Modifikation von M, die vor X in der einzigen totalen Ordnung erscheint, - eine nicht zusammenhängende Modifikation von M, die später in M's Modifikationsordnung erscheint.
Für ein Paar von atomaren Operationen auf M namens A und B, wobei A den Wert von M schreibt und B ihn liest, wenn es zwei memory_order_seq_cst atomic_thread_fences X und Y gibt und wenn A sequenziert-vor X liegt, Y sequenziert-vor B liegt 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 M's Modifikationsordnung erscheint.
Für zwei atomare Modifikationen von M namens A und B erscheint B nach A in M's Modifikationsordnung, wenn
- es eine
memory_order_seq_cstatomic_thread_fence X gibt, so dass A sequenziert-vor X liegt und X vor B in der Single Total Order erscheint, - oder es gibt eine
memory_order_seq_cstatomic_thread_fence Y, so dass Y sequenziert-vor B liegt und A vor Y in der Single Total Order erscheint, - oder es gibt
memory_order_seq_cstatomic_thread_fences X und Y, so dass A sequenziert-vor X liegt, Y sequenziert-vor B liegt und X vor Y in der Single Total Order erscheint.
Beachten Sie, dass dies bedeutet, dass
memory_order_seq_cst gekennzeichnet sind, ins Spiel kommen, die sequentielle Konsistenz verloren geht,Die sequentielle Ordnung kann für Situationen mit mehreren Produzenten und mehreren Konsumenten erforderlich sein, bei denen alle Konsumenten die Aktionen aller Produzenten in derselben Reihenfolge beobachten müssen.
Die 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.
[bearbeiten] Beziehung zu volatile
Innerhalb eines Ausführungsthreads können Zugriffe (Lese- und Schreibvorgänge) über volatile Lvalues nicht an beobachtbaren Nebeneffekten (einschließlich anderer volatiler Zugriffe) vorbeigereiht werden, die durch eine Sequenzpunkt innerhalb desselben Threads getrennt sind, aber diese Ordnung wird nicht garantiert von einem anderen Thread beobachtet, da volatile Zugriffe keine Inter-Thread-Synchronisation etablieren.
Darüber hinaus sind volatile Zugriffe nicht atomar (gleichzeitiges Lesen und Schreiben ist ein Datenrennen) und ordnen den Speicher nicht (nicht-volatile Speicherzugriffe können frei um den volatilen Zugriff herum neu geordnet 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 daher volatile für die Inter-Thread-Synchronisation verwendet werden kann. Standardmäßige volatile-Semantiken sind für die Multi-Threaded-Programmierung nicht anwendbar, obwohl sie für z. B. die Kommunikation mit einem Signalhandler, der im selben Thread ausgeführt wird, wenn sie auf sig_atomic_t-Variablen angewendet werden, ausreichend sind.
[bearbeiten] Beispiele
| Dieser Abschnitt ist unvollständig Grund: kein Beispiel |
[bearbeiten] Referenzen
- C23-Standard (ISO/IEC 9899:2024)
- 7.17.1/4 memory_order (S: TBD)
- 7.17.3 Order and consistency (S: TBD)
- C17-Standard (ISO/IEC 9899:2018)
- 7.17.1/4 memory_order (S: 200)
- 7.17.3 Order and consistency (S: 201-203)
- C11-Standard (ISO/IEC 9899:2011)
- 7.17.1/4 memory_order (S: 273)
- 7.17.3 Order and consistency (S: 275-277)
[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 |
| Dieser Abschnitt ist unvollständig Grund: Gute Referenzen zu QPI, MOESI und vielleicht Dragon finden. |