Die zentrale Idee von std::atomic_thread_fence ist es, Synchronisations- und Ordnungsbedingungen zwischen Threads ohne Operationen auf atomaren Variablen zu etablieren.
std::atomic_thread_fence werden in der englischen Literatur fences oder auch memory barriers genannt. Übersetze ich die Begriffe in die deutsche Sprache mit Zaun oder Speicherbarriere, so ist direkt offensichtlich, was die Aufgabe eines std::atomic_thread_fence ist.
Ein std::atomic_thread_fence im Sourcecode verhindert, dass bestimmte Operationen die Speicherbarriere überwinden können.
Speicherbarrieren
Bisher habe ich relativ unscharf von bestimmten Operationen gesprochen, die Speicherbarrieren nicht überwinden können. Von welcher Klasse von Operationen spreche ich? Operationen lassen sich in zwei Klassen von Operationen unterteilen: Lesen und Schreiben oder auch Laden und Speichern. Um nicht eine Verwirrung mit der etablierten englischsprachigen Begrifflichkeit einzuführen, spreche ich nun einfach von load und store. So ist der Ausdruck if (resultReady) return result ein Load gefolgt von einem Load, da sowohl resultReady als auch result gelesen werden.
Damit gibt es vier Kombinationsmöglichkeiten von load und store Operationen.
- LoadLoad: Ein Lesen gefolgt von einem Lesen.
- LoadStore: Ein Lesen gefolgt von einem Schreiben.
- StoreLoad: Ein Schreiben gefolgt von einem Lesen.
- StoreStore: Ein Schreiben gefolgt von einem Schreiben.
Natürlich gibt es auch komplexere Operationen, die sowohl aus einem Lesen und Schreiben bestehen (count++). Das widerspricht aber nicht meiner grundsätzlichen Klassifizierung.
Wo kommen nun die Speicherbarrieren ins Spiel? Werden Speicherbarrieren zwischen die zwei Operationen wie LoadLoad, LoadStore, StoreLoad oder auch StoreStore platziert, stellen sie sicher, dass bestimme LoadLoad-, LoadStore-, StoreLoad- oder auch StoreStore-Operationen nicht umsortiert werden können. Diese Gefahr der Umsortierung ist immer dann gegeben, wenn nicht-atomare Variablen oder atomare Variablen mit Relaxed-Semantik verwendet werden.
Drei Arten von Speicherbarrieren werden gerne verwendet. Hier muss ich wieder in die englischsprachige Begrifflichkeit wechseln. Das sind full fence, acquire fence und release fence. acquire ist eine Lade-Operatione, release ist eine Speicher-Operation. Was passiert nun, wenn einer der drei Speicherbarrieren (fence) zwischen einer der vier Kombinationsmöglichkeiten von Lade und Speicher-Operationen platziert wird?
- Full fence: Ein full fence std::atomic_thread_fence() zwischen beliebigen Operationen verhindert, dass dies umsortiert werden können. Das gilt aber mit Ausnahme von StoreLoad Operationen.
- Acquire fence: Ein acquire fence std:.atomic_thread_fence(std::memory_order_acquire) verhindert, dass eine Lese-Operation vor einem acquire fence mit einer Lese- oder Schreibe-Operation nach einem acquire fence umsortiert werden kann.
- Release fence: Ein release fence std::memory_thread_fence(std::memory_order_release) verhindert, dass eine Lese- oder Schreibe-Operation vor einem release fence mit einer Schreibe-Operationen nach einem release fence umsortiert werden kann.
Ich gebe zu, dass ich einige Energien aufwenden musste, um die Definition der acquire und release fence und ihre Konsequenzen für lockfreie Programme zu verstehen. Insbesondere die feinen Unterschiede zur Acquire-Release-Semantik von atomaren Operationen bergen einiges an Verwirrungspotential. Bevor ich aber dazu komme, will ich die Definitionen veranschaulichen.
Speicherbarrieren veranschaulicht
Welche Operationen können die Speicherbarrieren überwinden? Das zeigen die drei Graphiken. Dabei steht ein durchgestrichener Pfeil dafür, dass der entsprechende fence diese Operationen unterbindet.
Full fence
std::atomic_thread_fence() kann in bekannter Manier explizit als std::atomic_thread_fence(std::memory_order_seq_cst) geschrieben werden. Den per Default gilt die Sequenzielle Konsistenz. Wird die Sequenzielle Konsistenz beim full fence angewandt, besteht eine globale Ordnung auf den std::atomic_thread_fence Aufrufen.
Acquire fence
Release fence
Das drei Fences lassen sich noch komprimierter darstellen.
Speicherbarrieren auf einen Blick
Wie geht's weiter?
Das war die Theorie. Die Praxis folgt im nächsten Artikel. In diesem stelle ich im ersten Schritt einen acquire fence einer acquire Operation, ein release fence einer release Operation gegenüber. Im zweiten Schritt werde ich ein typisches Consumer-Producer Szenario mit Hilfe von acquire-release Operationen auf Speicherbarrieren (fences) portieren.
Go to Leanpub/cpplibrary "What every professional C++ programmer should know about the C++ standard library". Hole dir dein E-Book. Unterstütze meinen Blog.
Weiterlesen...