Bedingungsvariablen ermöglichen es, Threads über Benachrichtigungen zu synchronisieren. Damit lassen sich typische Anwendungsfälle wie Sender/Empfänger oder Producer/Consumer umsetzen. In diesen wartet der Empfänger oder Consumer, bis er eine Benachrichtigung vom Sender oder Producer erhält um mit seiner Arbeit fortzufahren.
std::condition_variable
Eine Bedingungsvariable kann sowohl die Rolle des Senders als auch des Empfängers annehmen. Als Sender kann er einen oder auch mehrere Empfänger benachrichtigen.
Mit diesem Wissen lassen sich bereits Bedingungsvariablen anwenden.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
#include <iostream> #include <condition_variable> #include <mutex> #include <thread> std::mutex mutex_; std::condition_variable condVar; void doTheWork(){ std::cout << "Processing shared data." << std::endl; } void waitingForWork(){ std::cout << "Worker: Waiting for work." << std::endl; std::unique_lock<std::mutex> lck(mutex_); condVar.wait(lck); doTheWork(); std::cout << "Work done." << std::endl; } void setDataReady(){ std::cout << "Sender: Data is ready." << std::endl; condVar.notify_one(); } int main(){ std::cout << std::endl; std::thread t1(waitingForWork); std::thread t2(setDataReady); t1.join(); t2.join(); std::cout << std::endl; } |
Das Programm besitzt zwei Kinderthreads t1 und t2, die ihre Arbeitspakete waitingForWork und setDataReady in Zeile 31 und 32 erhalten. Die Funktion setDataReady signalisiert durch die Bedingungsvariable condVar, dass sie mit der Vorbereitung des Arbeitspaketes fertig ist: condVar.notify_one(). Auf diese Benachrichtigung wartet der Thread t1 in seinem Arbeitspaket waitingForWork mit Hilfe seines Locks: condVar.wait(lck). Dabei durchläuft er immer die gleichen Schritte. Er wacht auf, versucht den Lock zu erhalten, prüft, während er das Lock hält, ob seine Benachrichtigung schon vorliegt und legt sich im Misserfolgsfall wieder zur Ruhe. Im Erfolgsfall verlässt er sein Hamsterrad und führt sein Arbeitspaket weiter aus.
Die Ausgabe des Programms ist nicht besonders spannend, spiegelt sie doch genau meine Beschreibung wider.
Zufälliges Aufwachen
Leider ist dem nicht so. Tatsächlich kann es vorkommen, dass der Empfänger mit seinem Arbeitspaket abschließt, bevor der Sender seine Benachrichtigung geschickt hat. Wie ist das möglich? Der Empfänger ist empfänglich für sogenannte spurious wakeups. Durch einen spurious wakeup wacht der Empfänger auf und interpretiert dies als vermeintliche Benachrichtigung. Um sich gegen das zufällige Aufwachen zu schützen, sollte die Synchronisation zusätzlich durch eine weitere Bedingung gesichert werden. Genau dies ist im nächsten Beispiel umgesetzt.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
#include <iostream> #include <condition_variable> #include <mutex> #include <thread> std::mutex mutex_; std::condition_variable condVar; bool dataReady; void doTheWork(){ std::cout << "Processing shared data." << std::endl; } void waitingForWork(){ std::cout << "Worker: Waiting for work." << std::endl; std::unique_lock<std::mutex> lck(mutex_); condVar.wait(lck,[]{return dataReady;}); doTheWork(); std::cout << "Work done." << std::endl; } void setDataReady(){ std::lock_guard<std::mutex> lck(mutex_); dataReady=true; std::cout << "Sender: Data is ready." << std::endl; condVar.notify_one(); } int main(){ std::cout << std::endl; std::thread t1(waitingForWork); std::thread t2(setDataReady); t1.join(); t2.join(); std::cout << std::endl; } |
Der wesentliche Unterschied zum ersten Beispiel ist es, dass die boolsche Variable dataReady in Zeile 9 als zusätzliche Bedingung verwendet wird. Dazu wird dataReady in setDataReady (Zeile 26) auf true gesetzt. Genau diesen Wert prüft die Bedingungsvariable in der Funktion waitingForWork : condVar.wait(lck,[]{return dataReady;}). Dazu besitzt die Methode condVar.wait eine weitere Variante, die ein Prädikat erwartet. Ein Prädikat ist eine aufrufbare Einheit, die die Antwort true oder false zurückgibt. In diesem konkreten Fall ist die aufrufbare Einheit eine Lambda-Funktion . Nun prüft die Bedingungsvariable des Threads t1 nicht nur, ob benachrichtigt wurde, sondern auch, ob dataReady wahr ist.
Eine kleine Anmerkung noch zu dataReady. dataReady ist ein Variable, die von zwei Threads gleichzeitig verwendet wird und modifiziert werden kann. Damit gilt es sie durch einen Lock zu schützen. Da Thread t1 sein Lock nur einmalig setzt und wieder freigibt, benötigt er nur einen einfachen lock_guard. Thread t2 hingegen setzt seinen Lock in der Regel mehrmals, daher benötigt er einen unique_lock.
Bedingungsvariablen bergen einige Herausforderungen. Sie müssen gelockt werden und sind empfänglich für zufälliges Aufwachen. Die meisten Anwendungsfälle lassen sich mit Tasks deutlich einfacher lösen. Dazu mehr im nächsten Artikel.
Lost wakeup
Die Gemeinheiten des Bedingungsvariablen hören noch nicht auf. Ungefähr bei jeder zehnten Ausführung des Programms kam es zu einem seltsamen Phänomen. Das Programm blockierte.
Dies Phänomen passte so gar nicht in mein Verständnis von Bedingungsvariablen. Habe ich schon erwähnt, dass ich Bedingungsvariablen nicht leiden kann. Dank Anthony Williams löste sich das Problem aber in Wohlgefallen auf.
Was ist das Problem? Schickt der Empfänger seine Benachrichtigung, bevor der Empfänger darauf wartet, wartet der Empfänger unter Umständen erfolglos. Ein kritischer Wettlauf um den wait-Aufruf tritt ein. Der C++-Standard beschreibt Bedingungsvariablen als Synchronisationsmechanismus zur gleichen Zeit: "The condition_variable class is a synchronization primitive that can be used to block a thread, or multiple threads at the same time, ...". Genau das passiert in diesem Fall. Die Nachricht des Senders geht verloren und der Empfänger wartet und wartet und ... .
Wie lässt sich das Problem lösen? Das Heilmittel Prädikat zur Vermeidung des spurious wakeups hilft auch beim lost wakeup. Evaluiert das Prädikat zu true, kann der Empfänger unabhängig von einer empfangenen Benachrichtigung seine Arbeit fortsetzen. Die Variable dataReady fungiert als ein Gedächtnis. Denn sobald die Variable dataReady in Zeile 26 auf true gesetzt ist, nimmt der Empfänger in Zeile 19 an, dass die Benachrichtigung bereits verschickt wurde.
Hintergrundinformationen
- Lambda-Funktionen
- Die Details zu Lambda-Funktionen lasse sich in dem Online-Artikel 12/2011 und 02/2012 auf Modernes C++ in der Praxis nachlesen.
-
Wie geht's weiter?
Mit Tasks wird im nächsten Artikel der Umgang mit Threads deutlich einfacher.
-
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...