Aus Linux-Magazin 10/2012

Modernes C++ in der Praxis – Folge 6

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.

Dieser Beitrag der C++11-Reihe knüpft an den im Linux-Magazin 08/12 an [1]. Er vollendet das Programmierbeispiel vom Boss und seinen sechs Mitarbeitern. Um die Arbeitskraft seiner Arbeiter in geregelte Bahnen zu lenken, setzt der Chef diesmal Promise (Versprechen) und Future (Zukunft) ein. Wie diese neuen Features funktionieren und warum sie die mühselige Synchronisation der Arbeiter-Threads durch atomare Variablen, Mutexe, Locks und Bedingungsvariablen überflüssig machen, zeigt dieser Artikel.

Compilerversionen

Nur der aktuelle GCC 4.7 kann die Codebeispiele aus diesem Artikel übersetzen. Denn die älteren GCC-Implementierungen setzen voraus, dass die Argumente des Thread kopiert werden. Weder Promise noch Future sind jedoch kopierbar.

Arbeiten auf Zuruf

Die Aufgabe, vor der der Boss steht, ist schnell skizziert (Abbildung 1) – Leser der vorigen Folgen kennen sie bereits: Die Arbeiter Herb, Scott, Bjarne, Andrei, Andrew und David benachrichtigen den Boss, sobald sie die Vorbereitungen für ihre Arbeit abgeschlossen haben. Sobald der Boss alle Benachrichtigungen erhalten hat, gibt er ihnen das Kommando, das Tagwerk zu beginnen. Jeder der sechs Arbeiter teilt später dem Boss mit, wenn er mit seiner Arbeit fertig ist.

Abbildung 1: Der Arbeitsablauf für die Arbeiter Herb, Scott, Bjarne, Andrei, Andrew und David.

Abbildung 1: Der Arbeitsablauf für die Arbeiter Herb, Scott, Bjarne, Andrei, Andrew und David.

Hat der Boss diese Nachricht von allen erhalten, ist das Arbeitspensum erledigt und er schickt alle in den Feierabend. Damit gibt es vier Synchronisationspunkte für ein Programm, das diesen Arbeitsablauf mittels mehrerer Threads abbildet: Zwei muss der Boss einhalten, die anderen beiden die Arbeiter.

Ein erster Versuch

Dieser Arbeitsablauf ist nicht so trivial, wie er klingt. Verständlich, dass der Boss zuerst einen Prototyp entwirft und diesen mit seinem Vorarbeiter Bjarne testet, zumal er mit Future und Promise vollkommenes Neuland betritt (Listing 1). Auf der informativen Wikiseite C++ Reference [2] lässt sich das ganze Leistungsspektrum des Paares Promise [3] und Future [4] nachlesen. Das Ergebnis des Prototyps sieht vielversprechend aus. Das Paar »promise« in Zeile 36 und »future« in Zeile 39 baut einen Datenkanal auf, in dem Promise als Datensender und Future als Datenempfänger dient.

Listing 1

Prototyp

01 #include <future>
02 #include <iostream>
03 #include <thread>
04 #include <utility>
05
06 using std::cout;
07 using std::endl;
08
09 using std::string;
10 using std::move;
11
12 using std::promise;
13 using std::future;
14
15 using std::thread;
16
17 class Worker{
18 public:
19   Worker(string n):name(n){};
20
21     void operator() (future<string>&& boss2Worker){
22
23       // still waiting for the permission to start working
24       string message= boss2Worker.get();
25       cout << "Worker from Boss: " << message << endl;
26     }
27 private:
28   string name;
29 };
30
31 int main(){
32
33   cout << endl;
34
35   // define the promise => Instruction from the boss
36   promise<string> startWorkPromise;
37
38   // get the futures from the promise
39   future<string> startWorkFuture= startWorkPromise.get_future();
40
41   Worker bjarne("Bjarne");
42   thread bjarneWork(bjarne,move(startWorkFuture));
43
44   // notify the worker about the begin of the work
45   cout << "Boss: Notifying the worker. \n" << endl;
46   startWorkPromise.set_value("START YOUR WORK!");
47
48   bjarneWork.join();
49
50   cout << endl;
51
52 }

Dabei ist es weder notwendig, dass die beiden Kommunikationsendpunkte sich in verschiedenen Threads befinden, noch müssen tatsächlich Daten geschickt werden. Beispielsweise kann der Promise auch nur eine Benachrichtigung oder eine Ausnahme an den Future schicken. Exemplarisch sind Datensender und -empfänger in Abbildung 2 dargestellt.

Abbildung 2: Promise und Future als Sender und Empfänger von Nachrichten.

Abbildung 2: Promise und Future als Sender und Empfänger von Nachrichten.

Kommunikationskanal

Zeile 36 von Listing 1 erzeugt den Promise, der einen »string« als Rückgabewert besitzt. Durch die Methode »get_future()« in Zeile 39 gibt dieser einen Future zurück, der durch einen Kanal mit ihm verbunden ist. Dieser Future erwartet einen »string« vom Promise. Der Arbeiter-Thread Bjarne erhält den Future als Parameter. Dabei verschiebt Zeile 42 mit »move(startWorkFuture)« den Future in einen neuen Thread.

Den Future nimmt der Thread in Zeile 21 mit »future<string>&&« an, einer so genannten Rvalue-Referenz. Dies ist notwendig, da ein Future sich nicht kopieren lässt. Schreibt der Boss in Zeile 46 durch den Aufruf »startWorkPromise.set_value(“START YOUR WORK!”)« den Wert in den Kanal, steht dieser bereit, um vom Arbeiter abgeholt zu werden. Da der Aufruf von »boss2Worker.get()« in Zeile 24 blockiert, muss der Arbeiter darauf warten, dass der Boss den Wert explizit setzt. Dieses Warten auf den Wert des Promise kann ein Future durch eine absolute oder relative Zeitangabe einschränken.

Das Meisterwerk

Stolz präsentiert der Boss seinen neu entworfenen Arbeitsablauf mit Listing 2 und führt ihn gleich aus (Abbildung 3). Der wesentliche Unterschied zu früheren Versionen ist, dass er mit seinen Arbeitern vier Datenkanäle verwendet: zwei, um Nachrichten von den Arbeitern zu empfangen, und zwei, um ihnen seine Anweisungen zu erteilen. So erhält der Arbeiter-Thread »herbWork()« in Zeile 59 die zwei Promises »herbPrepared« und »herbDone« , mit denen Herb signalisiert, dass er die Arbeit vorbereitet beziehungsweise vollendet hat. Da Promises nicht kopierbar sind, müssen sie wie im Prototyp verschoben werden.

Listing 2

Promise und Future (gekürzt)

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() (promise<void>&& preparedWork, shared_future<void> boss2WorkerStartWork,
17 promise<void>&& doneWork, shared_future<void>boss2WorkerGoHome ){
18
19 // prepare the work and notfiy the boss
20 int prepareTime= getRandomTime(500,2000);
21 sleep_for(milliseconds(prepareTime));
22 preparedWork.set_value();
23 cout << name << ": " << "Work prepared after " << prepareTime << " milliseconds." << endl;
24
25 // still waiting for the permission to start working
26 boss2WorkerStartWork.wait();
27
28 // do the work and notify the boss
29 int workTime= getRandomTime(200,400);
30 sleep_for(milliseconds(workTime));
31 doneWork.set_value();
32 cout << name << ": " << "Work done after " << workTime << " milliseconds." << endl;
33
34 // still waiting for the permission to go home
35 boss2WorkerGoHome.wait();
36
37 }
38 private:
39 string name;
40 };
41
42 int main(){
43
44 cout << endl;
45
46 // define the promise => Instruction from the boss
47 promise<void> startWorkPromise;
48 promise<void> goHomePromise;
49
50 // get the shared futures from the promise
51 shared_future<void> startWorkFuture= startWorkPromise.get_future();
52 shared_future<void> goHomeFuture= goHomePromise.get_future();
53
54 promise<void> herbPrepared;
55 future<void> waitForHerbPrepared= herbPrepared.get_future();
56 promise<void> herbDone;
57 future<void> waitForHerbDone= herbDone.get_future();
58 Worker herb(" Herb");
59 thread herbWork(herb,move(herbPrepared),startWorkFuture,move(herbDone),goHomeFuture);
60
61 // start thread scott, bjarne, andrei, andrew and david
62 [...]
63
64 cout << "BOSS: PREPARE YOUR WORK.\n " << endl;
65
66 // waiting for the worker
67 waitForHerbPrepared.wait(), waitForScottPrepared.wait(), waitForBjarnePrepared.wait(), waitForAndreiPrepared.wait(), waitForAndrewPrepared.wait(), waitForDavidPrepared.wait();
68
69 // notify the worker about the begin of the work
70 cout << "\nBOSS: START YOUR WORK. \n" << endl;
71 startWorkPromise.set_value();
72
73 // waiting for the worker
74 waitForHerbDone.wait(), waitForScottDone.wait(), waitForBjarneDone.wait(), waitForAndreiDone.wait(), waitForAndrewDone.wait(), waitForDavidDone.wait();
75
76 // notify the worker about the end of the work
77 cout << "\nBOSS: GO HOME. \n" << endl;
78 goHomePromise.set_value();
79
80 herbWork.join();
81 scottWork.join();
82 bjarneWork.join();
83 andreiWork.join();
84 andrewWork.join();
85 davidWork.join();
86
87 }
Abbildung 3: Der fertige Arbeitsablauf mit den Nachrichten von Boss und sechs Arbeitern.

Abbildung 3: Der fertige Arbeitsablauf mit den Nachrichten von Boss und sechs Arbeitern.

Das Besondere an Herbs Benachrichtigung ist dieses Mal, dass er in den Zeilen 22 und 31 keinen Wert mitschickt, sondern nur den Boss benachrichtigt. Dies ist auch der Grund dafür, dass der Boss in Zeile 67 lediglich wartet, bis er Nachrichten von Herb und den restlichen Arbeitern erhält.

Um die Arbeiter zu informieren, dass sie mit der Arbeit beginnen beziehungsweise nach Hause gehen können, verwendet der Boss zwei »shared_future« , nämlich »startWorkFuture« und »goHomeFuture« . Während der Future »future« im Prototyp genau einen Sender mit einem Empfänger verbindet, erlaubt es der »shared_future« dem Boss, eine Benachrichtigung an alle Arbeiter zu verschicken (Zeilen 71 und 78). Im Gegensatz zum »future« ist der »shared_future« kopierbar, sodass man ihn nicht in den Arbeiter-Thread verschieben muss (Zeile 59). Listing 2 ist gekürzt wiedergegeben, die vollständige Version gibt es unter [5].

Wie geht es weiter?

Dieser Artikel über Promise und Future schließt die Miniserie über Multithreading in C++11 vorerst ab. Wer noch den einen oder anderen Artikel zum Thema wünscht, schreibt dem Autor unter mailto:cpp@linux-magazin.de.

Die Multithreading-Fähigkeit von C++11 hat nämlich noch ein paar interessante Features zu bieten. Dazu gehören das sichere Initialisieren von Daten in Threads, Thread-lokaler Speicher und insbesondere atomare Datentypen sowie das sehr anspruchsvolle C++11-Speichermodell. Dieses Modell definiert, in welcher Reihenfolge atomare Datenoperationen in einem Thread ausgeführt werden und wann diese Operationen zwischen den Threads sichtbar sind.

In den Erläuterungen zu Listing 1 hieß es relativ oberflächlich, dass sich weder Future noch Promise kopieren lassen, sie müssten verschoben werden. Diese einstweilige Oberflächlichkeit hat einen Grund: Der nächste Artikel dieser Reihe wird sich genau diesem Thema widmen. Er erläutert die erwähnten Rvalue-Referenzen und deren Anwendung in der Move-Semantik sowie beim Perfect Forwarding.

Move-Semantik erlaubt es, an der Performance-Schraube eines Programms zu drehen, da sie das aufwändige Kopieren eines Objekts durch das wirtschaftliche Verschieben ersetzt. Perfect Forwarding hingegen löst ein recht einfaches, aber in C++ bisher ungelöstes Problem: Funktionsargumente generisch annehmen und unverändert weitergeben. (mhu)

Infos

  1. Rainer Grimm, “Im Gleichtakt”: Linux-Magazin 08/12, S. 88
  2. C++ Reference: http://en.cppreference.com/w/cpp
  3. Promise: http://en.cppreference.com/w/cpp/thread/promise
  4. Future: http://en.cppreference.com/w/cpp/thread/future
  5. Vollständige Quelltext-Listings zu diesem Artikel:https://www.linux-magazin.de/static/listings/magazin/2012/10/cpp

Der Autor

Rainer Grimm arbeitet seit 1999 als Software-Entwickler bei der Science + Computing AG in Tübingen. Insbesondere hält er Schulungen für das hauseigene Produkt SC Venus. Im Dezember 2011 ist sein Buch “C++11: Der Leitfaden für Programmierer zum neuen Standard” im Verlag Addison-Wesley erschienen.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 3 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben