165 votes

Quelle est la différence entre packaged_task et async ?

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 j'exécutais std::async con std::launch::deferred mais y en a-t-il une dans ce cas ?

Quelle est la différence entre ces deux approches et, surtout, dans quels cas dois-je utiliser l'une plutôt que l'autre ?

199voto

Zeta Points 34033

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 emballée

A packaged_task ne 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'un autre côté, std::async con launch::async essaiera d'exécuter la tâche dans un fil 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";

Drawback

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

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 du vrai asynchrone, vous devez garder 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, voir l'article d'Herb Sutter (en anglais). async y ~future qui décrit le problème, et celui de Scott Meyer. std::futures de std::async ne sont pas spéciales qui décrit les idées. Notez également que ce comportement a été spécifié dans C++14 et plus mais aussi couramment mis en œuvre dans C++11.

Autres différences

En utilisant std::async vous ne pouvez plus exécuter votre tâche sur un thread spécifique, où std::packaged_task peuvent être déplacées vers d'autres fils.

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

En outre, un packaged_task doit être invoqué avant que vous n'appeliez f.get() sinon votre programme se bloquera car le futur 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

Utilisez std::async si vous voulez que certaines choses soient faites et que vous ne vous souciez pas vraiment de savoir quand elles seront faites, et std::packaged_task si vous voulez conclure des choses afin de les déplacer vers d'autres fils ou de les appeler plus tard. Ou, pour citer Christian :

Au final, un std::packaged_task est juste une fonctionnalité de niveau inférieur pour la mise en œuvre de std::async (c'est pourquoi il peut faire plus que std::async s'il est utilisé avec d'autres éléments de niveau inférieur, tels que std::thread ). En clair, un std::packaged_task est un std::function lié à un std::future y std::async enveloppe et appelle un std::packaged_task (éventuellement dans un autre fil).

11 votes

Vous devriez ajouter que le futur renvoyé par async se bloque à la destruction (comme si vous aviez appelé get) alors que celui renvoyé par packaged_task ne le fait pas.

26 votes

Au final, un std::packaged_task est juste une fonctionnalité de niveau inférieur pour la mise en œuvre de std::async (c'est pourquoi il peut faire plus que std::async s'il est utilisé avec d'autres éléments de niveau inférieur, tels que std::thread ). Tout simplement a parlé std::packaged_task est un std::function lié à un std::future y std::async enveloppe et appelle un std::packaged_task (éventuellement dans un autre fil).

0 votes

Je fais quelques expériences sur le bloc ~future(). Je n'ai pas pu reproduire l'effet de blocage sur la destruction de l'objet futur. Tout a fonctionné de manière asynchrone. J'utilise VS 2013 et lorsque je lance l'asynchrone, j'utilise std::launch::async. Est-ce que VC++ a en quelque sorte "corrigé" ce problème ?

7voto

Daniel Langr Points 841

TL;DR

std::packaged_task nous permet d'obtenir le std::future "limité" à certains Appelable et de contrôler quand et où cet appelant sera exécuté sans avoir besoin de cet objet futur.

std::async permet le premier, mais pas le second. En d'autres termes, il nous permet d'obtenir le futur d'un callable, mais nous n'avons aucun contrôle sur son exécution sans cet objet futur.

Exemple pratique

Voici un exemple pratique d'un problème qui peut être résolu avec std::packaged_task mais pas avec std::async .

Considérez que vous voulez mettre en œuvre un réserve de fils . Il se compose d'un nombre fixe de fils de travail et un file d'attente partagée . Mais une file d'attente partagée de quoi ? std::packaged_task est tout à fait approprié ici.

template <typename T>
class ThreadPool {
public:
  using task_type = std::packaged_task<T()>;

  std::future<T> enqueue(task_type task) {
      // could be passed by reference as well...
      // ...or implemented with perfect forwarding
    std::future<T> res = task.get_future();
    { std::lock_guard<std::mutex> lock(mutex_);
      tasks_.push(std::move(task));
    }
    cv_.notify_one();
    return res;
  }

  void worker() { 
    while (true) {  // supposed to be run forever for simplicity
      task_type task;
      { std::unique_lock<std::mutex> lock(mutex_);
        cv_.wait(lock, [this]{ return !this->tasks_.empty(); });
        task = std::move(tasks_.top());
        tasks_.pop();
      }
      task();
    }
  }
  ... // constructors, destructor,...
private:
  std::vector<std::thread> workers_;
  std::queue<task_type> tasks_;
  std::mutex mutex_;
  std::condition_variable cv_;
};

Une telle fonctionnalité ne peut être mise en œuvre avec std::async . Nous devons retourner un std::future de enqueue() . Si nous avons appelé std::async là (même avec différé ) et retourner std::future nous n'aurions alors aucune possibilité d'exécuter l'appelant en worker() . Notez que vous ne pouvez pas créer plusieurs futures pour le même état partagé (les futures ne sont pas copiables).

1voto

Tâche packagée vs asynchrone

p> La tâche emballée contient une tâche [function or function object] et le couple futur/promis. Lorsque la tâche exécute une instruction de retour, elle provoque set_value(..) sur le packaged_task La promesse de la Commission.

a> Avec Future, promise et package task, nous pouvons créer des tâches simples sans trop nous soucier des threads [le thread est juste quelque chose que nous donnons pour exécuter une tâche].

Cependant, nous devons prendre en compte le nombre de threads à utiliser ou si une tâche doit être exécutée sur le thread actuel ou sur un autre, etc. async() qui décide de créer un nouveau fil de discussion, de recycler un ancien fil de discussion ou de simplement exécuter la tâche sur le fil de discussion actuel. Il renvoie un futur .

0voto

Radoslav.B Points 17

"Le modèle de classe std::packaged_task enveloppe toute cible appelable (fonction, expression lambda, expression de liaison, ou autre fonction ) afin qu'elle puisse être invoquée de manière asynchrone. Sa valeur de retour ou l'exception levée est stockée dans un état partagé auquel on peut accéder par le biais d'objets std::future".

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X