Open Source im professionellen Einsatz
Linux-Magazin 08/2012

Modernes C++ in der Praxis – Folge 5

Im Gleichtakt

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.

972

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.

Diesen Artikel als PDF kaufen

Express-Kauf als PDF

Umfang: 4 Heftseiten

Preis € 0,99
(inkl. 19% MwSt.)

Linux-Magazin kaufen

Einzelne Ausgabe
 
Abonnements
 
TABLET & SMARTPHONE APPS
Bald erhältlich
Get it on Google Play

Deutschland

Ähnliche Artikel

  • C++11

    Promise und Future erweisen sich als nützliche Neuerungen im C++11-Standard. Das Gespann macht die bisher aufwändige Synchronisation mehrerer Threads einfach und übersichtlich.

  • Sauber eingefädelt

    Der Standard für Threads unter Linux ist heute die Native Posix Threads Library (NPTL). Die Bibliothek überzeugt durch große Kompatibilität zum Standard und hohe Performance. Dieser Artikel untersucht die neue Threading-Engine und zeigt, wie Benutzeranwendungen davon profitieren.

  • C++11

    Die neue Zeitbibliothek von C++11 erweist sich als elementarer Bestandteil der Threading-Schnittstelle: Sowohl Threads, Locks und Bedingungsvariablen als auch Futures haben ein Verständnis von Zeit. Dank ihrer Unterstützung kann ein Entwickler unterschiedliche Wartestrategien verfolgen.

  • C++11

    2014 ist ein besonderes Jahr für C++. Drei Jahre nach C++11 erfährt der Sprachstandard mit C++14 den letzten Feinschliff. Neben generischen Lambda-Funktionen und der vereinfachten Ermittlung des Rückgabetyps kann C++14 vor allem mit einem Feature punkten: Reader-Writer-Locks.

  • Java-Threads

    Seit der ersten Version von Java sind Threads ein fester Bestandteil der Sprache. Das macht vieles einfacher als in anderen Programmiersprachen. Neuere Versionen der Java-Bibliothek bieten darüber hinaus viele nützliche Klassen für Locking und Synchronisierung.

comments powered by Disqus

Ausgabe 10/2017

Digitale Ausgabe: Preis € 6,40
(inkl. 19% MwSt.)

Artikelserien und interessante Workshops aus dem Magazin können Sie hier als Bundle erwerben.