Quelle est la différence entre packaged_task et async

134

En travaillant avec le modèle threadé de C ++ 11, j'ai remarqué que

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';

et

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

semblent faire exactement la même chose. Je comprends qu'il pourrait y avoir une différence majeure si je courais std::asyncavec std::launch::deferred, mais y en a-t-il une dans ce cas?

Quelle est la différence entre ces deux approches et, plus important encore, dans quels cas d'utilisation dois-je utiliser l'une par rapport à l'autre?

Nijansen
la source

Réponses:

161

En fait, l'exemple que vous venez de donner montre les différences si vous utilisez une fonction assez longue, telle que

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

Tâche packagée

A packaged_taskne démarre pas tout seul, vous devez l'invoquer:

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

D'autre part, std::asyncavec launch::asyncessaiera d'exécuter la tâche dans un thread différent:

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";

Inconvénient

Mais avant d'essayer de l'utiliser asyncpour tout, gardez à l'esprit que le futur retourné a un état partagé spécial, qui exige que cela future::~futurebloque:

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;
*/

Donc, si vous voulez un vrai asynchrone, vous devez conserver le retour future, ou si vous ne vous souciez pas du résultat si les circonstances changent:

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

Pour plus d'informations à ce sujet, consultez l'article de Herb Sutter asyncet~future , qui décrit le problème, et Scott Meyer std::futuresde std::asyncne sont pas spéciaux , qui décrit les idées. Notez également que ce comportement a été spécifié dans C ++ 14 et versions ultérieures , mais aussi couramment implémenté dans C ++ 11.

Autres différences

En utilisant, std::asyncvous ne pouvez plus exécuter votre tâche sur un thread spécifique, où std::packaged_taskpeut être déplacé vers d'autres threads.

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";

De plus, a packaged_taskdoit être invoqué avant d'appeler f.get(), sinon votre programme se figera car l'avenir ne sera jamais prêt:

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

TL; DR

À utiliser std::asyncsi vous voulez que certaines choses soient faites et std::packaged_taskque vous ne vous souciez pas vraiment du moment où elles sont terminées, et si vous voulez terminer les choses afin de les déplacer vers d'autres threads ou de les appeler plus tard. Ou, pour citer Christian :

En fin de compte, a std::packaged_taskest juste une fonctionnalité de niveau inférieur à implémenter std::async(c'est pourquoi elle peut faire plus que std::asyncsi elle est utilisée avec d'autres éléments de niveau inférieur, comme std::thread). Simplement parlé a std::packaged_taskest std::functionlié à a std::futureet std::asyncencapsule et appelle a std::packaged_task(éventuellement dans un fil différent).

Zeta
la source
9
Vous devez ajouter que le futur retourné par async se bloque lors de la destruction (comme si vous appeliez get) alors que celui renvoyé par packaged_task ne le fait pas.
John5342
23
En fin de compte, a std::packaged_taskest juste une fonctionnalité de niveau inférieur à implémenter std::async(c'est pourquoi elle peut faire plus que std::asyncsi elle est utilisée avec d'autres éléments de niveau inférieur, comme std::thread). Simplement parlé a std::packaged_taskest std::functionlié à a std::futureet std::asyncencapsule et appelle a std::packaged_task(éventuellement dans un fil différent).
Christian Rau
Je fais des expériences sur le bloc ~ future (). Je n'ai pas pu reproduire l'effet de blocage sur la future destruction d'objets. Tout fonctionnait de manière asynchrone. J'utilise VS 2013 et quand je lance l'async, j'ai utilisé std :: launch :: async. VC ++ a-t-il "résolu" ce problème d'une manière ou d'une autre?
Frank Liu
1
@FrankLiu: Eh bien, N3451 est une proposition acceptée, qui (pour autant que je sache) est entrée dans C ++ 14. Étant donné que Herb fonctionne chez Microsoft, je ne serais pas surpris que cette fonctionnalité soit implémentée dans VS2013. Un compilateur qui suit strictement les règles C ++ 11 afficherait toujours ce comportement.
Zeta le
1
@Mikhail Cette réponse précède à la fois C ++ 14 et C ++ 17, donc je n'avais pas les standards mais seulement des propositions sous la main. Je vais supprimer le paragraphe.
Zeta
1

Tâche packagée vs asynchrone

p> La tâche packagée contient une tâche[function or function object]et une paire future / promesse. Lorsque la tâche exécute une instruction return, elle provoqueset_value(..)sur lapackaged_taskpromesse de.

a> Étant donné la tâche Future, Promise et Package, nous pouvons créer des tâches simples sans trop se soucier des threads [le thread est juste quelque chose que nous donnons pour exécuter une tâche].

Cependant, nous devons considérer le nombre de threads à utiliser ou si une tâche est mieux exécutée sur le thread actuel ou sur un autre, etc. Ces décisions peuvent être gérées par un lanceur de thread appelé async(), qui décide s'il faut créer un nouveau thread ou recycler un ancien ou exécutez simplement la tâche sur le thread actuel. Il renvoie un avenir.

maxshuty
la source
0

"Le modèle de classe std :: packaged_task encapsule toute cible appelable (fonction, expression lambda, expression de liaison ou autre objet fonction) afin qu'elle puisse être appelée de manière asynchrone. Sa valeur de retour ou l'exception levée est stockée dans un état partagé accessible via les objets std :: future. "

"La fonction modèle async exécute la fonction f de manière asynchrone (potentiellement dans un thread séparé) et renvoie un std :: future qui contiendra finalement le résultat de cet appel de fonction."

Radoslav.B
la source