Die C++11-Reihe beschäftigt sich weiter mit dem Synchronisieren von Threads. Diesmal setzt der Chef-Thread Bedingungsvariablen ein, um die Tätigkeit seiner Mitarbeiter-Threads zu koordinieren.
Multicore-Rechner und Multithreading gehören zum modernen IT-Alltag. Doch die Arbeit mehrerer Threads will koordiniert sein. Dieser Artikel greift das Programmierbeispiel aus der vorigen Folge wieder auf, in dem ein Chef-Thread sechs Mitarbeiter per Zuruf zu koordinieren versuchte. Zunächst riefen sie alle durcheinander – erst als der Boss die Regel ausgab, dass jeder Arbeiter seinen Kollege zuerst ausreden lassen soll, kehrte Ordnung ein. Dabei repräsentierte je ein Thread einen Arbeiter, und der Ausgabekanal »std::cout« stand für die gemeinsam genutzte Variable, die es zu schützen gilt [1].
Synchronisation per Wahrheitswert
Statt mit Zurufen möchte der Boss seine Mitarbeiter nun mit Hilfe eines Wahrheitswerts synchronisieren. Er hat sich das ganz einfach vorgestellt: Ein Arbeiter beginnt genau dann zu arbeiten, wenn der Wahrheitswert auf »true« gesetzt ist. Da dieser Wert zu Anfang den Wert »false« besitzt, kann der Boss seinen Arbeitern das Startsignal geben. Der erste Prototyp eines kleinen Arbeitsablaufs geht in C++11 schnell von der Hand, wie Listing 1 zeigt. Die Codebeispiele sind aus Platzgründen gekürzt, unter [2] steht der vollständige Quelltext zum Download bereit.
Listing 1
Synchronisation per Wahrheitswert
01 [...]
02
03 mutex notifiedMutex;
04 bool notified;
05
06 void worker(){
07
08 cout << "Worker: Waiting for Notification." << endl;
09
10 unique_lock<mutex> lck(notifiedMutex);
11
12 while(!notified){
13
14 lck.unlock();
15 sleep_for(milliseconds(1000));
16 lck.lock();
17
18 }
19
20 cout << "Worker: Get Notification." << endl;
21
22 }
23
24 void boss(){
25
26 cout << "Boss: Notify Worker." << endl;
27 lock_guard<std::mutex> lck(notifiedMutex);
28 notified=true;
29
30 }
31
32 int main(){
33
34 cout << endl;
35
36 std::thread workerThread(worker);
37 std::thread bossThread(boss);
38
39 workerThread.join();
40 bossThread.join();
41
42 cout << endl;
43
44 }
Das war einfach, denkt der Boss. In Zeile 27 verpackt er den Mutex »notifiedMutex« in einen »lock_guard« . Dies stellt sicher, dass nur ein einziger Thread die kritische Region in Zeile 28 betreten darf.
Der Arbeiter-Thread in den Zeilen 6 bis 22 hat wesentlich mehr zu tun. In Zeile 10 sperrt er den Mutex »notified_mutex« mit einem »unique_lock« . Dies ist notwendig, da ein »unique_lock« mehrmals freigeben wie auch sperren kann. Damit ist der »unique_lock« deutlich mächtiger als der »lock_guard« , den der Chef in Zeile 27 einsetzt, denn dieser bindet den Mutex im Konstruktoraufruf.
Das eigentliche Tagwerk beginnt für den Arbeiter-Thread in der »while« -Schleife (Zeilen 12 bis 18). Solange der Wahrheitswert »notified« nicht »true« ist, führt er immer den gleichen monotonen Job aus: Er gibt den Mutex frei, schläft für 1 Sekunde und bindet den Mutex wieder. Zum einen achtet er darauf, dass selbst das Lesen des Wahrheitswerts »notified« durch den Mutex »notifiedMutex« geschützt ist, zum anderen darauf, dass er den Mutex in Zeile 15 für 1 Sekunde freigibt, damit der Boss den Wert auf »true« setzen kann.
Obwohl der Boss alles richtig gemacht hat (Abbildung 1), ist er mit seinem Prototyp nicht zufrieden. Ihn stört, dass bis zu einer ganzen Sekunde vergeht, bis der Arbeiter auf eine Benachrichtigung reagiert. Der Arbeiter fühlt sich hingegen für seinen Job überqualifiziert, da er seine ganze Arbeitsenergie auf das immerwährende Kontrollieren des Wahrheitswerts verschwenden muss.

Abbildung 1: Die erste Implementierung: Die Synchronisation über einen gemeinsamen Wahrheitswert funktioniert.
Hinsichtlich der Threads hat der Vorgesetzte bei seiner Implementierung folgende zwei Anforderungen an einen Benachrichtigungsmechanismus nicht gut bedient:
- Die Zeit zwischen dem Senden und dem Empfangen der Benachrichtigung soll möglichst kurz sein.
- Der wartende Thread soll möglichst wenig CPU-Zeit beanspruchen.
Der Boss ist wieder in der Pflicht und sieht sich die Bedingungsvariablen in C++11 genauer an, denn diese setzen die beiden Anforderungen deutlich besser um. Einerseits wacht der Empfänger bei Benachrichtigung sofort auf, andererseits verwendet er wenig CPU-Zeit auf das Warten.
Bedingungsvariablen
Bedingungsvariablen bieten zwei Typen von Operationen: Sie erlauben es, wahlweise ein Signal zu senden oder auf eins zu warten. Dabei kann der Adressat einer Benachrichtigung ein einzelner (»notify_one()« ) oder mehrere Threads (»notify_all()« ) sein, die sich im wartenden Zustand (»wait()« ) befinden. Sendet ein Thread eine Benachrichtigung an einen wartenden Thread, so wacht der Empfänger-Thread auf und fährt mit seiner Arbeit fort. Der Empfänger muss dabei die gleiche Bedingungsvariable benützen wie der Sender-Thread.
Mit dem Arbeitsablauf in Listing 2 sind der Arbeiter und der Boss deutlich zufriedener. Zuerst zum Boss: Durch den Aufruf »condVar.notify_one()« in Zeile 21 schickt er seine Benachrichtigung an einen Arbeiter. Dieser wartet in Zeile 11 darauf und fährt genau dann mit seiner Arbeit fort, wenn er die Nachricht erhält. Dabei verwenden beide den gleichen Mutex »notifiedMutex« .
Listing 2
Synchronisation per Bedingungsvariable
01 [...]
02
03 mutex notifiedMutex;
04 condition_variable condVar;
05 bool notified;
06
07 void worker(){
08
09 cout << "Worker: Waiting for Notification." << endl;
10 unique_lock<mutex> lck(notifiedMutex);
11 condVar.wait(lck,[]{return notified;});
12 cout << "Worker: Get Notification." << endl;
13
14 }
15
16 void boss(){
17
18 cout << "Boss: Notify Worker." << endl;
19 lock_guard<std::mutex> lck(notifiedMutex);
20 notified=true;
21 condVar.notify_one();
22
23 }
24
25 int main(){
26
27 cout << endl;
28
29 std::thread workerThread(worker);
30 std::thread bossThread(boss);
31
32 workerThread.join();
33 bossThread.join();
34
35 cout << endl;
36
37 }
Etwas befremdlich wirkt, dass der Boss und der Arbeiter weiter den Wahrheitswert »notified« verwenden. Tatsächlich erhält der Arbeiter nicht nur die Benachrichtigung, sondern versichert sich zusätzlich im »wait()« -Aufruf (Zeile 11) durch die Lambda-Funktion »[]{return notified;}« [3], dass der Wert auf »true« gesetzt ist. Das ist notwendig, weil es vorkommt, dass der Arbeiter irrtümlich eine Benachrichtigung erhält. Dieses Phänomen ist unter dem Namen Spurious Wakeups [4] bekannt. Der Wahrheitswert »notified« verhindert, dass der Thread wegen eines solchen Fehlalarms seine Arbeit aufnimmt.
Synchronisation der Arbeiterschaft
Die weiteren Details rund um Bedingungsvariablen hat der Boss unter [5] nachgelesen. Nun will er sie im großen Stil für die Koordination seiner Arbeiter einsetzen. In Abbildung 2 ist sein Plan dargestellt. Jeder Arbeiter bereitet zuerst seine Arbeit vor und gibt dem Boss Bescheid, sobald er fertig ist. Sind alle Arbeiter mit der Vorbereitung fertig, signalisiert der Boss allen gleichzeitig, dass sie mit ihrem Tagwerk beginnen können. Nun gilt es für den Boss, zu warten, bis jeder Arbeiter ihm zurückgemeldet hat, dass er sein Arbeitspaket erledigt hat. Zum Schluss schickt er Herb, Scott, Bjarne, Andrei, Andrew und David mit »GO HOME« in den Feierabend. Der Boss hat es geschafft. Der komplexe Arbeitsablauf seiner Arbeiter läuft geordnet ab (Abbildung 3).

Abbildung 2: Mit Hilfe von Benachrichtigungen lässt sich der Arbeitsablauf effizient gestalten: Die Arbeiter geben Bescheid, wenn sie bereit oder fertig sind, der Chef gibt das Kommmando für Arbeit und Feierabend.

Abbildung 3: Dank Bedingungsvariablen klappt der Arbeitsablauf wie am Schnürchen. Die Arbeiter warten auf den Boss und dieser auf die Arbeiter.
Ein Blick auf den Sourcecode in Listing 3 zeigt: Um die sechs Arbeiter zu koordinieren, muss der Boss einiges an Buchhaltung erledigen. Neben Bedingungsvariablen kommen Locks, Mutexe und auch atomare Variablen zum Einsatz. Atomare Variablen bieten atomare Lese- und Schreib-Operationen an, sodass sie von mehreren Threads gleichzeitig ohne weiteren Schutzmechanismus modifiziert werden dürfen. Sie sind Bestandteil es neuen C++11-Standards [6]. Dabei stehen nicht nur Wahrheitswerte (»std::atomic_bool« ), sondern auch Zeichen (»std::atomic_char« ) und natürliche Zahlen (»std::atomic_int« ) in verschiedenen Variationen als atomare Datentypen zu Verfügung.
Listing 3
Synchronisieren mit Bedingungsvariablen
01 [...]
02
03 int getRandomTime(int start, int end){
04
05 random_device seed;
06 mt19937 engine(seed());
07 uniform_int_distribution<int> dist(start,end);
08
09 return dist(engine);
10 };
11
12 class Worker{
13 public:
14 Worker(string n):name(n){};
15
16 void operator() (){
17
18 // prepare the work and notfiy the boss
19 int prepareTime= getRandomTime(500,2000);
20 sleep_for(milliseconds(prepareTime));
21 preparedCount++;
22 cout << name << ": " << "Work prepared after " << prepareTime << " milliseconds." << endl;
23 worker2BossCondVariable.notify_one();
24
25 { // in order to release the lock startWorkMutex
26
27 // wait for the start notification of the boss
28 unique_lock<mutex> startWorkLock( startWorkMutex );
29 boss2WorkerCondVariable.wait( startWorkLock,[]{ return startWork; });
30
31 }
32
33 // do the work and notify the boss
34 int workTime= getRandomTime(200,400);
35 sleep_for(milliseconds(workTime));
36 doneCount++;
37 cout << name << ": " << "Work done after " << workTime << " milliseconds." << endl;
38 worker2BossCondVariable.notify_one();
39
40 { // in order to release the lock goHomeMutex
41
42 // wait for the go home notification of the boss
43 unique_lock<mutex> goHomeLock( goHomeMutex );
44 boss2WorkerCondVariable.wait( goHomeLock,[]{ return goHome;});
45
46 }
47
48 }
49 private:
50 string name;
51 };
52
53 int main(){
54
55 cout << endl;
56
57 Worker herb(" Herb");
58 thread herbWork(herb);
59
60 // start thread scott, bjarne, andrei, andrew and david
61 [...]
62
63 cout << "BOSS: PREPARE YOUR WORK.\n " << endl;
64
65 // waiting for the worker
66 preparedCount.store(0);
67 unique_lock<mutex> preparedUniqueLock( preparedMutex );
68 worker2BossCondVariable.wait(preparedUniqueLock,[]{ return preparedCount == 6; });
69
70 // notify the worker about the begin of the work
71 startWork= true;
72 cout << "\nBOSS: START YOUR WORK. \n" << endl;
73 boss2WorkerCondVariable.notify_all();
74
75 // waiting for the worker
76 doneCount.store(0);
77 unique_lock<mutex> doneUniqueLock( doneMutex );
78 worker2BossCondVariable.wait(doneUniqueLock,[]{ return doneCount == 6; });
79
80 // notify the worker about the end of the work
81 goHome= true;
82 cout << "\nBOSS: GO HOME. \n" << endl;
83 boss2WorkerCondVariable.notify_all();
84
85 herbWork.join();
86 scottWork.join();
87 bjarneWork.join();
88 andreiWork.join();
89 andrewWork.join();
90 davidWork.join();
91
92 }
Mit Hilfe des neuen Klassentemplates »std::atomic<T>« kann der erfahrene C++-Entwickler zudem einen eigenen Datentyp definieren, der den Datentyp »T« um atomare Eigenschaften erweitert. Datenstrukturen, die durch den Einsatz von atomaren Datentypen gänzlich ohne Locks und Mutexe auskommen, werden als Lock-free bezeichnet. Tiefere Einsichten vermittelt der Artikel [7] von Anthony Williams.
In Listing 3 initiiert der Boss den Arbeitsablauf, indem er seine Arbeiter ab Zeile 60 startet. Anschließend wartet er in Zeile 68, bis alle Arbeiter melden, dass sie ihre Arbeit vorbereitet haben. Er verwendet die atomare Variable »preparedCount« , um die Anzahl der Nachrichten zu zählen.
Sind alle Arbeiter mit ihrer Vorbereitung fertig, gibt er mit »boss2WorkerCondVariable.notfiy_all()« (Zeile 73) allen die Benachrichtigung, dass sie mit ihrer Arbeit beginnen können. Nun gilt es wieder für den Boss, in Zeile 78 zu warten, bis alle ihr Tagwerk vollendet haben. Ist dies geschehen, schickt er durch seine Benachrichtigung Herb, Scott, Bjarne, Andrei, Andrew und David nach Hause.
Arbeitsablauf
Der Gang der Ereignisse ist bei den Arbeitern genau spiegelbildlich zu denen beim Boss. Als Beispiel soll Bjarnes Arbeitstag dienen. Hat Bjarne die Vorbereitung seiner Arbeit abgeschlossen – ein Zufallswert bestimmt in Zeile 19, wie lange sie dauert –, inkrementiert er die atomare Variable »preparedCount« in Zeile 21 und gibt anschließend dem Boss durch »worker2BossCondVariable.notify_one()« Bescheid, dass er fertig ist. Nun wartet er in Zeile 29 auf die Nachricht vom Boss, dass er mit seiner Arbeit beginnen kann. Erhält er diesen Bescheid, beginnt für ihn das Tagwerk und die Ereignisse wiederholen sich. In Zeile 38 teilt er dem Boss mit, dass er mit seiner Arbeit fertig ist. In Zeile 44 wartet er schließlich darauf, dass er endlich Feierabend hat.
Wenn es klemmt
Zu Recht ist der Boss stolz auf seinen ausgefeilten Arbeitsplan. Dabei sei nicht verschwiegen, dass er bei seinem ersten Lösungsansatz den künstlichen Bereich um den Lock »startWorkLock« von Zeile 25 bis 31 und genauso um den Lock »goHomeLock« von Zeile 40 bis 46 vergessen hatte. Das Ergebnis war ein Deadlock (Abbildung 4).

Abbildung 4: Die Arbeiter blockieren sich gegenseitig, weil der erste noch den steuernden Mutex hält.
Die Ursache: Der erste Arbeiter, der den Mutex »startWorkMutex« in Zeile 28 verwendet, bindet diesen und wartet in Zeile 44 darauf, dass er in den Feierabend gehen darf. Er wartet, hält aber dabei noch den Mutex. Damit harren die anderen Arbeiter vergeblich auf den Mutex und der ganze Arbeitsablauf klemmt. Die zunächst vergessenen künstlichen Bereiche in den Zeilen 25 bis 31 und 40 bis 46 lösen das Problem: »unique_lock« verliert seine Gültigkeit und gibt automatisch seinen Mutex frei.
Wie geht’s weiter?
Die anfängliche Euphorie des Chefs ist allmählich der Skepsis gewichen. Zwar hat sich sein Programm zur Koordination der Arbeiter in den letzten Wochen im Großen und Ganzen bewährt – sieht man von dem Vorfall ab, als die Arbeiter ihre »prepared« -Nachricht schon verschickten, bevor sich der Boss in den Wartezustand begeben hatte. Das Ergebnis war, dass die Arbeiter vergeblich auf ihre Benachrichtigung zum Arbeitsbeginn warteten.
Den Boss stört aber vor allem, dass die ganze Koordination der Arbeiter viel zu komplex ist. Sie erfordert Sychronisationsmittel wie etwa Bedingungsvariablen, Locks, Mutexe, ja sogar atomare Variablen. Kein Wunder, dass sich Fehler einschleichen. Da entdeckt er beim Schmökern im neuen C++11-Standard die Futures. Sie erlauben es, in Programmen auf die Vollendung einer Aufgabe zu warten. Die nächste Folge dieser Artikelserie wird zeigen, ob es dem Boss mit Futures tatsächlich leichter von der Hand geht, seine sechs Mitarbeiter zu koordinieren.
Infos
- Rainer Grimm, “Gemeinsam ins Ziel”: Linux-Magazin 06/12, S. 90
- Listings zum Artikel: https://www.linux-magazin.de/static/listings/magazin/2012/08/cpp/
- Rainer Grimm, “Kurz und knackig”: Linux-Magazin 02/12, S. 92
- Spurios Wakeups: http://en.wikipedia.org/wiki/Spurious_wakeup
- Condition Variable: http://en.cppreference.com/w/cpp/thread/condition_variable
- Header »<atomic>« : http://www.stdthread.co.uk/doc/headers/atomic.html
- Lock-free: http://www.justsoftwaresoluti- ons.co.uk/threading/non_blocking_lock_free_and_wait_free.html






