Warum unterscheidet sich std :: future von std :: packaged_task und std :: async?
Ich habe erfahren, warum der Grund, futureaus dem ich zurückgekehrt bin, einen std::asyncbesonderen gemeinsamen Zustand hat, durch den wait on returned futureder Zerstörer der Zukunft geschehen ist. Aber wenn wir verwenden std::pakaged_task, zeigt seine Zukunft nicht das gleiche Verhalten. Um eine verpackte Aufgabe abzuschließen, müssen Sie explizit nennen get()auf futureObjekt aus packaged_task.
Jetzt sind meine Fragen:
- Was könnte die interne Umsetzung der Zukunft sein (Denken
std::asyncvs.std::packaged_task)? - Warum wurde das gleiche Verhalten nicht auf die
futureRückgabe von angewendetstd::packaged_task? Oder mit anderen Worten, wie wird das gleiche Verhalten gestopptstd::packaged_taskfuture?
Den Kontext finden Sie im folgenden Code:
Es wartet nicht, bis die countdownAufgabe beendet ist. Wenn ich jedoch den Kommentar abbringe // int value = ret.get();, wird dies beendet countdownund ist offensichtlich, da wir die zurückgegebene Zukunft buchstäblich blockieren.
// packaged_task example
#include <iostream> // std::cout
#include <future> // std::packaged_task, std::future
#include <chrono> // std::chrono::seconds
#include <thread> // std::thread, std::this_thread::sleep_for
// count down taking a second for each value:
int countdown (int from, int to) {
for (int i=from; i!=to; --i) {
std::cout << i << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
std::cout << "Lift off!" <<std::endl;
return from-to;
}
int main ()
{
std::cout << "Start " << std::endl;
std::packaged_task<int(int,int)> tsk (countdown); // set up packaged_task
std::future<int> ret = tsk.get_future(); // get future
std::thread th (std::move(tsk),10,0); // spawn thread to count down from 10 to 0
// int value = ret.get(); // wait for the task to finish and get result
std::cout << "The countdown lasted for " << std::endl;//<< value << " seconds.\n";
th.detach();
return 0;
}
Wenn ich eine std::asyncAufgabe countdownin einem anderen Thread ausführe , wird die Aufgabe immer beendet , unabhängig davon, ob ich sie get()für ein zurückgegebenes futureObjekt verwende oder nicht .
// packaged_task example
#include <iostream> // std::cout
#include <future> // std::packaged_task, std::future
#include <chrono> // std::chrono::seconds
#include <thread> // std::thread, std::this_thread::sleep_for
// count down taking a second for each value:
int countdown (int from, int to) {
for (int i=from; i!=to; --i) {
std::cout << i << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
std::cout << "Lift off!" <<std::endl;
return from-to;
}
int main ()
{
std::cout << "Start " << std::endl;
std::packaged_task<int(int,int)> tsk (countdown); // set up packaged_task
std::future<int> ret = tsk.get_future(); // get future
auto fut = std::async(std::move(tsk), 10, 0);
// int value = fut.get(); // wait for the task to finish and get result
std::cout << "The countdown lasted for " << std::endl;//<< value << " seconds.\n";
return 0;
}
Antworten
std::asynchat genaues Wissen darüber, wie und wo die ihm übertragene Aufgabe ausgeführt wird. Das ist seine Aufgabe: die Aufgabe auszuführen . Um das zu tun, muss es tatsächlich irgendwo platziert werden. Das könnte irgendwo ein Thread-Pool sein, ein neu erstellter Thread oder an einem Ort, der von jemandem ausgeführt werden kann, der den Thread zerstört future.
Da sie asyncweiß, wie die Funktion ausgeführt wird, verfügt sie über 100% der Informationen, die sie benötigt, um einen Mechanismus zu erstellen, der kommunizieren kann, wenn diese potenziell asynchrone Ausführung abgeschlossen ist, und um sicherzustellen, dass, wenn Sie die Funktion zerstören future, welcher Mechanismus auch immer verwendet wird Wenn Sie diese Funktion ausführen, wird sie schließlich tatsächlich ausgeführt. Immerhin weiß es, was dieser Mechanismus ist.
Aber packaged_task nicht . Alles, packaged_taskwas Sie tun müssen, ist, ein aufrufbares Objekt zu speichern, das mit den angegebenen Argumenten aufgerufen werden kann, ein Objekt promisemit dem Typ des Rückgabewerts der Funktion zu erstellen und ein Mittel bereitzustellen, um a futureabzurufen und die Funktion auszuführen, die den Wert generiert.
Wann und wo die Aufgabe tatsächlich ausgeführt wird, geht nichts an packaged_task. Ohne dieses Wissen kann die Synchronisation, die erforderlich ist, um futureden Destruktor mit der Aufgabe zu synchronisieren, einfach nicht erstellt werden.
Angenommen, Sie möchten die Aufgabe in einem frisch erstellten Thread ausführen. OK, um seine Ausführung mit der futureZerstörung des zu synchronisieren , benötigen Sie einen Mutex, den der Destruktor blockiert, bis der Task-Thread beendet ist.
Aber was ist, wenn Sie die Aufgabe im selben Thread wie der Aufrufer des futureDestruktors ausführen möchten ? Nun, dann können Sie keinen Mutex verwenden, um das zu synchronisieren, da sich alles auf demselben Thread befindet. Stattdessen muss der Destruktor die Aufgabe aufrufen. Das ist ein völlig anderer Mechanismus, und er hängt davon ab, wie Sie ihn ausführen möchten.
Da Sie packaged_tasknicht wissen, wie Sie es ausführen möchten, kann es nichts davon tun.
Beachten Sie, dass dies nicht nur für gilt packaged_task. Alle future s, die aus einem vom Benutzer erstellten promiseObjekt erstellt wurden , haben nicht die spezielle Eigenschaft von async' futures.
Die Frage sollte also wirklich sein, warum asyncdies so funktioniert und nicht, warum alle anderen dies nicht tun .
Wenn Sie das wissen möchten, liegt dies an zwei konkurrierenden Anforderungen: Es asyncmusste eine einfache, hirntote einfache Methode sein, um eine asynchrone Ausführung zu erhalten (für die Sychronisierung bei Zerstörung sinnvoll ist), und niemand wollte eine neue erstellen futureTyp, der bis auf das Verhalten seines Destruktors mit dem vorhandenen identisch war. Deshalb haben sie beschlossen, die Funktionsweise zu überlasten future, was die Implementierung und Verwendung erschwert.
@Nicol Bolas hat diese Frage bereits recht zufriedenstellend beantwortet . Daher werde ich versuchen, die Frage aus einer anderen Perspektive zu beantworten und die bereits von @Nicol Bolas erwähnten Punkte herauszuarbeiten.
Das Design verwandter Dinge und ihre Ziele
Betrachten Sie diese einfache Funktion, die wir auf verschiedene Arten ausführen möchten:
int add(int a, int b) {
std::cout << "adding: " << a << ", "<< b << std::endl;
return a + b;
}
Vergessen std::packaged_task, std ::futureund std::asyncfür eine Weile, lassen Sie uns einen Schritt zurück und überdenken , wie std::functionfunktioniert und was Problem sie verursacht.
Fall 1 - std::functionist nicht gut genug, um Dinge in verschiedenen Threads auszuführen
std::function<int(int,int)> f { add };
Sobald wir haben f, können wir es im selben Thread ausführen, wie:
int result = f(1, 2); //note we can get the result here
Oder in einem anderen Thread wie diesem:
std::thread t { std::move(f), 3, 4 };
t.join();
Wenn wir genau hinschauen, stellen wir fest, dass die Ausführung fin einem anderen Thread ein neues Problem darstellt: Wie erhalten wir das Ergebnis der Funktion? Die Ausführung fim selben Thread hat dieses Problem nicht - wir erhalten das Ergebnis als zurückgegebenen Wert, aber wenn wir es in einem anderen Thread ausführen, haben wir keine Möglichkeit, das Ergebnis zu erhalten. Genau das wird gelöst std::packaged_task.
Fall 2 - std::packaged_tasklöst das Problem, das std::functionnicht gelöst werden kann
Insbesondere wird ein Kanal zwischen Threads erstellt, um das Ergebnis an den anderen Thread zu senden. Abgesehen davon ist es mehr oder weniger dasselbe wie std::function.
std::packaged_task<int(int,int)> f { add }; // almost same as before
std::future<int> channel = f.get_future(); // get the channel
std::thread t{ std::move(f), 30, 40 }; // same as before
t.join(); // same as before
int result = channel.get(); // problem solved: get the result from the channel
Jetzt sehen Sie, wie std::packaged_taskdas Problem von gelöst wird std::function. Dies bedeutet jedoch nicht, dass std::packaged_taskdies in einem anderen Thread ausgeführt werden muss. Sie können es auch im selben Thread ausführen, genau wie std::function, obwohl Sie das Ergebnis immer noch vom Kanal erhalten.
std::packaged_task<int(int,int)> f { add }; // same as before
std::future<int> channel = f.get_future(); // same as before
f(10, 20); // execute it in the current thread !!
int result = channel.get(); // same as before
Grundsätzlich std::functionund std::packaged_taskähnlich: Sie wickeln einfach eine aufrufbare Entität ein, mit einem Unterschied: Sie std::packaged_taskist Multithreading-freundlich, da sie einen Kanal bereitstellt, über den das Ergebnis an andere Threads weitergegeben werden kann. Beide führen die umschlossene aufrufbare Entität NICHT selbst aus. Man muss sie entweder im selben Thread oder in einem anderen Thread aufrufen , um die umschlossene aufrufbare Entität auszuführen. Grundsätzlich gibt es in diesem Raum zwei Arten von Dingen:
- was ausgeführt wird, dh regelmäßige Funktionen
std::function,std::packaged_taskusw. - Wie / Wo wird ausgeführt, dh Threads, Thread-Pools, Executoren usw.
Fall 3: std::asyncist eine ganz andere Sache
Es ist eine andere Sache, weil es kombiniert, was ausgeführt wird mit wie / wo ausgeführt wird .
std::future<int> fut = std::async(add, 100, 200);
int result = fut.get();
Beachten Sie, dass in diesem Fall der erstellten Zukunft ein Executor zugeordnet ist. Dies bedeutet, dass die Zukunft irgendwann abgeschlossen sein wird, da jemand Dinge hinter den Kulissen ausführt. Im Falle der von erstellten Zukunft std::packaged_taskgibt es jedoch nicht unbedingt einen Executor, und diese Zukunft wird möglicherweise nie abgeschlossen, wenn die erstellte Aufgabe niemals einem Executor übergeben wird.
Ich hoffe, das hilft Ihnen zu verstehen, wie die Dinge hinter den Kulissen funktionieren. Siehe die Online-Demo .
Der Unterschied zwischen zwei Arten von std::future
Nun, an diesem Punkt wird ziemlich klar, dass es zwei Arten gibt, von std::futuredenen geschaffen werden kann:
- Eine Art kann von erstellt werden
std::async. Eine solche Zukunft hat einen zugeordneten Vollstrecker und kann somit vervollständigen. - Andere Arten können durch
std::packaged_taskoder solche Dinge geschaffen werden . Eine solche Zukunft hat nicht notwendigerweise einen zugeordneten Testamentsvollstrecker und kann daher abgeschlossen sein oder nicht.
Da im zweiten Fall der Zukunft nicht unbedingt ein Executor zugeordnet sein muss, ist der Destruktor nicht für die Fertigstellung / Wartezeit ausgelegt, da er möglicherweise niemals abgeschlossen wird:
{
std::packaged_task<int(int,int)> f { add };
std::future<int> fut = f.get_future();
} // fut goes out of scope, but there is no point
// in waiting in its destructor, as it cannot complete
// because as `f` is not given to any executor.
Ich hoffe, diese Antwort hilft Ihnen, die Dinge aus einer anderen Perspektive zu verstehen.
Die Verhaltensänderung ist auf den Unterschied zwischen std::threadund zurückzuführen std::async.
Im ersten Beispiel haben Sie durch Trennen einen Daemon-Thread erstellt. Wo Sie std::cout << "The countdown lasted for " << std::endl;in Ihrem Hauptthread drucken , kann vor, während oder nach den Druckanweisungen innerhalb der countdownThread-Funktion auftreten. Da der Haupt-Thread nicht auf den gespawnten Thread wartet, werden Sie wahrscheinlich nicht einmal alle Ausdrucke sehen.
Im zweiten Beispiel starten Sie die Thread-Funktion mit der std::launch::deferredRichtlinie. Das Verhalten für std :: async ist:
Wenn die asynchrone Richtlinie ausgewählt ist, wird der zugehörige Thread-Abschluss synchronisiert - mit der erfolgreichen Rückgabe der ersten Funktion, die auf den gemeinsam genutzten Status wartet, oder mit der Rückgabe der letzten Funktion, die den gemeinsam genutzten Status freigibt , je nachdem, was zuerst eintritt.
In diesem Beispiel haben Sie zwei Futures für denselben gemeinsamen Status. Bevor ihre dtors beim Beenden von main aufgerufen werden, muss die asynchrone Aufgabe abgeschlossen sein. Selbst wenn Sie keine Futures explizit definiert haben, bedeutet die temporäre Zukunft, die erstellt und zerstört wird (vom Aufruf an zurückgegeben std::async), dass die Aufgabe abgeschlossen ist, bevor der Hauptthread beendet wird.
Hier ist ein großartiger Blog-Beitrag von Scott Meyers, der das Verhalten von std::future& verdeutlicht std::async.
Verwandte SO Beitrag .