Was ist der Unterschied zwischen packaged_task und async

134

Bei der Arbeit mit dem Thread-Modell von C ++ 11 ist mir das aufgefallen

std::packaged_task<int(int,int)> task([](int a, int b) { return a + b; });
auto f = task.get_future();
task(2,3);
std::cout << f.get() << '\n';

und

auto f = std::async(std::launch::async, 
    [](int a, int b) { return a + b; }, 2, 3);
std::cout << f.get() << '\n';

scheinen genau das Gleiche zu tun. Ich verstehe , dass es könnte einen großen Unterschied, wenn ich lief std::asyncmit std::launch::deferred, aber gibt es eine in diesem Fall?

Was ist der Unterschied zwischen diesen beiden Ansätzen und was noch wichtiger ist, in welchen Anwendungsfällen sollte ich einen über den anderen verwenden?

Nijansen
quelle

Antworten:

161

Tatsächlich zeigt das Beispiel, das Sie gerade gegeben haben, die Unterschiede, wenn Sie eine ziemlich lange Funktion verwenden, wie z

//! sleeps for one second and returns 1
auto sleep = [](){
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return 1;
};

Gepackte Aufgabe

A packaged_taskstartet nicht von alleine, Sie müssen es aufrufen:

std::packaged_task<int()> task(sleep);

auto f = task.get_future();
task(); // invoke the function

// You have to wait until task returns. Since task calls sleep
// you will have to wait at least 1 second.
std::cout << "You can see this after 1 second\n";

// However, f.get() will be available, since task has already finished.
std::cout << f.get() << std::endl;

std::async

Auf der anderen Seite wird std::asyncwith launch::asyncversuchen, die Aufgabe in einem anderen Thread auszuführen:

auto f = std::async(std::launch::async, sleep);
std::cout << "You can see this immediately!\n";

// However, the value of the future will be available after sleep has finished
// so f.get() can block up to 1 second.
std::cout << f.get() << "This will be shown after a second!\n";

Nachteil

Bevor Sie jedoch versuchen, asyncalles zu verwenden, denken Sie daran, dass die zurückgegebene Zukunft einen speziellen gemeinsamen Zustand hat, der Folgendes erfordert future::~future:

std::async(do_work1); // ~future blocks
std::async(do_work2); // ~future blocks

/* output: (assuming that do_work* log their progress)
    do_work1() started;
    do_work1() stopped;
    do_work2() started;
    do_work2() stopped;
*/

Wenn Sie also wirklich asynchron sein möchten, müssen Sie die Rückgabe beibehalten future, oder wenn Sie sich nicht für das Ergebnis interessieren, wenn sich die Umstände ändern:

{
    auto pizza = std::async(get_pizza);
    /* ... */
    if(need_to_go)
        return;          // ~future will block
    else
       eat(pizza.get());
}   

Weitere Informationen zu diesem Thema finden Herb Sutter Artikel asyncund~future , die das Problem beschreibt, und Scott Meyers std::futuresvon std::asyncnicht speziell sind , die die Erkenntnisse beschreibt. Beachten Sie auch, dass dieses Verhalten in C ++ 14 und höher angegeben , aber auch häufig in C ++ 11 implementiert wurde.

Weitere Unterschiede

Mit können std::asyncSie Ihre Aufgabe nicht mehr für einen bestimmten Thread ausführen, der in std::packaged_taskandere Threads verschoben werden kann.

std::packaged_task<int(int,int)> task(...);
auto f = task.get_future();
std::thread myThread(std::move(task),2,3);

std::cout << f.get() << "\n";

Außerdem muss ein packaged_taskaufgerufen werden, bevor Sie aufrufen f.get(), da sonst Ihr Programm einfriert, da die Zukunft niemals bereit wird:

std::packaged_task<int(int,int)> task(...);
auto f = task.get_future();
std::cout << f.get() << "\n"; // oops!
task(2,3);

TL; DR

Verwenden std::asyncSie diese Option, std::packaged_taskwenn Sie möchten, dass einige Dinge erledigt werden und es Ihnen egal ist, wann sie erledigt sind, und wenn Sie Dinge einpacken möchten, um sie in andere Threads zu verschieben oder später aufzurufen. Oder um Christian zu zitieren :

Am Ende std::packaged_taskist a nur eine Funktion auf niedrigerer Ebene für die Implementierung std::async(weshalb es mehr kann, als std::asyncwenn es zusammen mit anderen Dingen auf niedrigerer Ebene verwendet wird std::thread). Einfach gesprochen std::packaged_taskist a ein std::functionLink zu a std::futureund std::asyncumschließt und ruft a auf std::packaged_task(möglicherweise in einem anderen Thread).

Zeta
quelle
9
Sie sollten hinzufügen, dass die Zukunft, die von asynchronen Blöcken bei der Zerstörung zurückgegeben wird (als ob Sie get aufgerufen hätten), die von packaged_task zurückgegebene nicht.
John5342
22
Am Ende std::packaged_taskist a nur eine Funktion auf niedrigerer Ebene für die Implementierung std::async(weshalb es mehr kann, als std::asyncwenn es zusammen mit anderen Dingen auf niedrigerer Ebene verwendet wird std::thread). Einfach gesprochen std::packaged_taskist a ein std::functionLink zu a std::futureund std::asyncumschließt und ruft a auf std::packaged_task(möglicherweise in einem anderen Thread).
Christian Rau
Ich mache einige Experimente mit dem Block ~ future (). Ich konnte den Blockierungseffekt auf die zukünftige Objektzerstörung nicht replizieren. Alles funktionierte asynchron. Ich verwende VS 2013 und als ich den Async starte, habe ich std :: launch :: async verwendet. Hat VC ++ dieses Problem irgendwie "behoben"?
Frank Liu
1
@FrankLiu: Nun, N3451 ist ein akzeptierter Vorschlag, der (soweit ich weiß) in C ++ 14 eingegangen ist. Angesichts der Tatsache, dass Herb bei Microsoft arbeitet, wäre ich nicht überrascht, wenn diese Funktion in VS2013 implementiert wäre. Ein Compiler, der sich strikt an die C ++ 11-Regeln hält, zeigt dieses Verhalten weiterhin an.
Zeta
1
@Mikhail Diese Antwort geht sowohl C ++ 14 als auch C ++ 17 voraus, daher hatte ich nicht die Standards, sondern nur Vorschläge zur Hand. Ich werde den Absatz entfernen.
Zeta
1

Gepackte Aufgabe vs asynchron

p> Gepackte Aufgabe enthält ein[function or function object]Paar ausAufgabeund Zukunft / Versprechen. Wenn die Task eine return-Anweisung ausführt, wirdset_value(..)daspackaged_taskVersprechendes Task eingehalten.

a> Angesichts der Zukunft, des Versprechens und der Paketaufgabe können wir einfache Aufgaben erstellen, ohne uns zu viele Gedanken über Threads zu machen [Thread ist nur etwas, das wir geben, um eine Aufgabe auszuführen].

Wir müssen jedoch überlegen, wie viele Threads verwendet werden sollen oder ob eine Aufgabe am besten auf dem aktuellen Thread oder auf einem anderen usw. ausgeführt werden kann. Solche Entscheidungen können von einem Thread-Launcher namens aufgerufen werden async(), der entscheidet, ob ein neuer Thread erstellt oder ein alter recycelt wird eine oder führen Sie die Aufgabe einfach auf dem aktuellen Thread aus. Es gibt eine Zukunft zurück.

maxshuty
quelle
0

"Die Klassenvorlage std :: packaged_task umschließt jedes aufrufbare Ziel (Funktion, Lambda-Ausdruck, Bindungsausdruck oder ein anderes Funktionsobjekt) so, dass es asynchron aufgerufen werden kann. Der zurückgegebene Rückgabewert oder die ausgelöste Ausnahme wird in einem gemeinsam genutzten Zustand gespeichert, auf den zugegriffen werden kann durch std :: zukünftige Objekte. "

"Die asynchrone Vorlagenfunktion führt die Funktion f asynchron aus (möglicherweise in einem separaten Thread) und gibt eine std :: future zurück, die schließlich das Ergebnis dieses Funktionsaufrufs enthält."

Radoslav.B
quelle