Der erzeugende Thread muss sich um die Lebenszeit seines erzeugten Threads (th) kümmern. Der Erzeuger wartet entweder, bis sein Kind seine Arbeit vollzogen hat (th.join()) oder er trennt sich von ihm (th.detach()). Das ist alles ein alter Hut. Dies gilt aber nicht für std:.async. Der große Charme von std::async besteht auch darin, dass sich der Erzeuger nicht um die Lebenszeit seines Kindes kümmern muss.
Fire and forget
std:async erzeugt einen besonderen Future. Dieser wartet automatisch in seinem Destruktor, bis die Arbeit seines assoziierten Promise fertig ist. Daher muss sich der Erzeuger nicht um die Lebenszeit seines Kindes kümmern. Es wird noch besser. Ein std::future kann als eine fire and forget Aufgabe direkt ausgeführt werden. Der durch std::async erzeugte Future wird direkt an Ort und Stelle ausgeführt. Weil der durch std::async erzeugte Future fut in dieser syntaktischen Form natürlich nicht an eine Variable gebunden wird, kann auf dem Future auch nicht fut.get() oder fut.wait() aufgerufen werden, um das Ergebnis seines Promise abzuholen.
Meine letzten Zeilen besitzen einiges an Verwirrungspotential, daher stelle ich einen konventionellen Future einem fire and forget Future gegenüber. Für den fire and forget Future ist es notwendig, dass sein Promise in einem eigenen Thread läuft, damit dieser sofort mit seinem Arbeitspaket beginnt. Das erreiche ich durch die std::launch::async Policy. Die Details zur Start-Policy eines Promise lassen sich in dem Artikel Asynchrone Funktionsaufrufe nachlesen.
auto fut= std::async([]{return 2011;});
std::cout << fut.get() << std::endl; /// 2011
std::async(std::launch::async,[]{std::cout << "fire and forget" << std::endl;}); // fire and forget
Die fire and forget Future besitzen einen großen Charme. Sie werden an Ort und Stelle gestartet und führen ihr Arbeitspaket aus, ohne dass sich sein Erzeuger um sie kümmern muss. Das einfache Beispiel zeigt dies Verhalten.
// async.cpp
#include <iostream>
#include <future>
int main() {
std::cout << std::endl;
std::async([](){std::cout << "fire and forget" << std::endl;});
std::cout << "main done " << std::endl;
}
Ohne weitere Worte, die Ausgabe des Programms.
Der Preis für dieses Mehr an Komfort ist aber hoch. Zu hoch.
Alle brav nacheinander
Der von std::async erzeugte Future wartet in seinem Destruktor darauf, bis seine Arbeit fertig ist. Ein anderes Wort für warten ist blockieren. Tatsächlich blockiert der Future den Fortschritt des Programms in seinem Destruktor. Besonders deutlich wird dies, wenn fire and forget Futures verwendet werden.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// blocking.cpp
#include <chrono>
#include <future>
#include <iostream>
#include <thread>
int main(){
std::cout << std::endl;
std::async(std::launch::async,[]{
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "first thread" << std::endl;
});
std::async(std::launch::async,[]{
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "second thread" << std::endl;}
);
std::cout << "main thread" << std::endl;
}
|
Im Programm werden die zwei Promise in eigenen Threads gestartet. Die resultierenden Future sind fire and forget Futures. Diese blockieren in ihrem Destruktor, bis der assoziierte Promise fertig ist. Das Ergebnis ist, dass die Promise mit großer Wahrscheinlichkeit in der Reihenfolge ausgeführt werden, in der sie im Sourcecode stehen. Genau dies Verhalten zeigt die Ausgabe des Programms.
Ich will den Punkt gerne nochmals auf den Punkt bringen. Obwohl ich im main-Thread zwei Promise erzeuge, die in eigenen Threads ausgeführt werden, wird jeder Thread brav einer nach dem anderen ausgeführt. Daher ist es auch zu erklären, dass der Thread mit dem längsten Arbeitspaket (Zeile 12) als erstes fertig ist. Wenn das nicht ernüchternd ist, anstatt drei Threads gleichzeitig auszuführen, führe ich jeden Thread nach dem anderen aus.
Dies konzeptionelle Problem, dass ein durch ein std::async erzeugter Future in seinem Destruktor darauf wartet, bis sein assoziierter Promise fertig ist, lässt sich nicht lösen. Dieses Problem lässt sich nur entschärfen. Wird der Future an eine Variable gebunden, findet das Blockieren in seinem Destruktor erst dann statt, wenn die Variable ihre Gültigkeit verliert. Genau dieses Verhalten zeigt die kleine Variation des Programms.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// notBlocking.cpp
#include <chrono>
#include <future>
#include <iostream>
#include <thread>
int main(){
std::cout << std::endl;
auto first= std::async(std::launch::async,[]{
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "first thread" << std::endl;
});
auto second= std::async(std::launch::async,[]{
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "second thread" << std::endl;}
);
std::cout << "main thread" << std::endl;
}
|
Jetzt entspricht die Ausgabe des Programms wieder unserer Intuition von den drei gleichzeitig ausgeführten Threads. Die Futures first (Zeile 12) und second (Zeile 17) besitzen eine Gültigkeit bis zum Ende der main-Funktion (Zeile 24). Erst zu diesem Zeitpunkt blockieren gegebenenfalls ihre Destruktoren. Das Ergebnis ist, dass die Threads mit dem kleineren Arbeitspaket schneller fertig sind.
Die Entwarnung
Zugegeben, ich habe die durch std::async erzeugten Futures sehr speziell eingesetzt. Zum einen sind die Future nicht an eine Variable gebunden, zum anderen werden die Future konsequenterweise nicht dazu verwendet, das Ergebnis des Promise mit einem blockierenden get oder wait Aufruf anzufordern. Genau unter diesen speziellen Randbedingungen wird das Phänomen sichtbar, dass der Future in seinem Destruktor blockiert.
Meine primäre Absicht dieses Beitrages war es, zu zeigen, dass ein durch std::async erzeugter fire and forget Future, der nicht an eine Variable gebunden wird, mit bedacht verwendet werden muss. Diese Aussage gilt aber nicht für Futures, die durch std::packaged_task oder durch std::promise erzeugt werden.
Wie geht's weiter?
Ein wichtiges Anliegen ist es mir, Bedingungsvariablen den Tasks zur Synchronisations von Threads gegenüber zustellen. Wichtig, da Task in der Form von Promise und Future meist die bessere Wahl sind. Besser, da Tasks deutlich einfacher in der Anwendung sind und daher viel weniger Gefahrenpotential bergen als Bedingungsvariablen. Im nächsten Artikel folgt der Vergleich.
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...