In dem Artikel sequenzielle Konsistenz habe ich bereits das Defaul-Speichermodell vorgestellt. Dieses Modell, in dem die Aktionen aller Threads einer globalen Reihenfolge oder auch eines globalen Zeittaks folgen, besitzt einen großen Vorteil und einen großen Nachteil.
Aufwändige Synchronisation
Der große Vorteil ist es, dass sich die sequenzielle Konsistenz mit unserer Intuition des Verhaltens mehrerer Threads deckt, der große Nachteil ist, dass die Synchronisation aller Threads aufwändig vom System sicherzustellen ist.
Das kleine Programm synchronisiert seinen Producer-Thread mit seinem Consumer-Thread mit Hilfe der sequentiellen Konsistenz.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <atomic> #include <iostream> #include <string> #include <thread> std::string work; std::atomic<bool> ready(false); void consumer(){ while(!ready.load()){} std::cout<< work << std::endl; } void producer(){ work= "done"; ready=true; } int main(){ std::thread prod(producer); std::thread con(consumer); prod.join(); con.join(); } |
Die Ausgabe des Programms ist kurz uns prägnant, aber dennoch interessant.
Dank der sequenziellen Konsistenz ist der Programmlauf vollkommen deterministisch. Das Programm gibt immer "done" aus.
Die Graphik bringt es auf den Punkt. Der Consumer-Thread wartet in der while-Schleife, bis die atomare Variable ready den Wert true besitzt. Ist dies der Fall, fährt der Consumer Thread mit seiner Arbeit fort.
Dass das Programm immer "done" ausgibt, lässt sich einfach begründen. Ich nütze dazu die zwei Eigenschaften der sequenziellen Konsistenz aus. Zum einen werden die Anweisungen beider Threads in Sourcecodereihenfolge ausgeführt, zum andern sehen die Threads die Anweisungen des anderen Threads in der gleichen Reihenfolge. Damt folgen alle Operationen dem globalem Takt. Diesem Takt folgt auch - mit Hilfe der while (!ready.load()){}-Schleife - die Synchronisation des Producer-Threads mit dem Consumer-Thread .
Es geht aber auch deutlich formaler. In der Begrifflichkeit des Speichermodells lautet die Argumentationskette:
=> Steht für die Schlussfolgerung in den nächsten Zeilen.
- work= "done" ist sequenced-before ready=true => work= "done" happens-before ready=true
- while(!ready.load()){} ist sequenced-before std::cout<< work << std::end l => while(!ready.load()){} happens-before std::cout<< work << std::endl
- ready=true synchronizes-with while(!ready.load()){} => ready= true inter-thread happens-before while (!ready.load()){} => ready= true happens-before while (!ready.load()){}
=> Da die happens-before Relation transitiv ist, gilt: work= "done" happens-before ready= true happens-before while(!ready.load()){} happens-before std::cout<< work << std::endl
Von der Sequenzielle Konsistenz zur Acquire-Release-Semantik
Ein Thread sieht die Operationen eines anderen Threads und somit aller Threads in der gleichen Reihenfolge. Diese elemente Eigenschaft der sequenziellen Konsistenz gilt nicht mehr, wenn wir statt der sequenziellen Konsistenz die Acquire-Release Semantik für atomare Operationen auswählen. Hier geht C++ deutlich weiter als C# oder auch Java. Leider verlassen wir nun auch den Raum unserer natürlichen Intuition.
Bei der Acquire-Release Semantik findet die Synchronisation nicht zwischen Threads, sondern zwischen atomaren Variablen der Threads statt. So synchronisiert sich eine Schreib-Operation eines Threads mit einer Lese-Operation eines anderen Threads auf der gleichen atomaren Variable. Diese Synchronisationsbeziehungen auf der gleichen atomaren Variablen helfen, happens-before Relation zwischen atomaren Variablen und damit zwischen Threads zu erzeugen.
Wie geht's weiter?
Die Details zur Acquire-Release Semantik zusammen mit dem optimierten Spinlock aus dem Artikel Das atomare Flag folgen im nächsten Artikel.
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...