What is the difference between packaged_task and async

36,071

Solution 1

Actually the example you just gave shows the differences if you use a rather long function, such as

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

Packaged task

A packaged_task won't start on it's own, you have to invoke it:

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

On the other hand, std::async with launch::async will try to run the task in a different thread:

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

But before you try to use async for everything, keep in mind that the returned future has a special shared state, which demands that future::~future blocks:

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

So if you want real asynchronous you need to keep the returned future, or if you don't care for the result if the circumstances change:

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

For more information on this, see Herb Sutter's article async and ~future, which describes the problem, and Scott Meyer's std::futures from std::async aren't special, which describes the insights. Also do note that this behavior was specified in C++14 and up, but also commonly implemented in C++11.

Further differences

By using std::async you cannot run your task on a specific thread anymore, where std::packaged_task can be moved to other 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";

Also, a packaged_task needs to be invoked before you call f.get(), otherwise you program will freeze as the future will never become ready:

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

TL;DR

Use std::async if you want some things done and don't really care when they're done, and std::packaged_task if you want to wrap up things in order to move them to other threads or call them later. Or, to quote Christian:

In the end a std::packaged_task is just a lower level feature for implementing std::async (which is why it can do more than std::async if used together with other lower level stuff, like std::thread). Simply spoken a std::packaged_task is a std::function linked to a std::future and std::async wraps and calls a std::packaged_task (possibly in a different thread).

Solution 2

TL;DR

std::packaged_task allows us to get the std::future "bounded" to some callable, and then control when and where this callable will be executed without the need of that future object.

std::async enables the first, but not the second. Namely, it allows us to get the future for some callable, but then, we have no control of its execution without that future object.

Practical example

Here is a practical example of a problem that can be solved with std::packaged_task but not with std::async.

Consider you want to implement a thread pool. It consists of a fixed number of worker threads and a shared queue. But shared queue of what? std::packaged_task is quite suitable here.

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_;
};

Such functionality cannot be implemented with std::async. We need to return an std::future from enqueue(). If we called std::async there (even with deferred policy) and return std::future, then we would have no option how to execute the callable in worker(). Note that you cannot create multiple futures for the same shared state (futures are non-copyable).

Solution 3

Packaged Task vs async

p> Packaged task holds a task [function or function object] and future/promise pair. When the task executes a return statement, it causes set_value(..) on the packaged_task's promise.

a> Given Future, promise and package task we can create simple tasks without worrying too much about threads [thread is just something we give to run a task].

However we need to consider how many threads to use or whether a task is best run on the current thread or on another etc.Such descisions can be handled by a thread launcher called async(), that decides whether to create a new a thread or recycle an old one or simply run the task on the current thread. It returns a future .

Share:
36,071

Related videos on Youtube

nikolas
Author by

nikolas

Student of computer science and mathematics

Updated on November 13, 2021

Comments

  • nikolas
    nikolas over 2 years

    While working with the threaded model of C++11, I noticed that

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

    and

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

    seem to do exactly the same thing. I understand that there could be a major difference if I ran std::async with std::launch::deferred, but is there one in this case?

    What is the difference between these two approaches, and more importantly, in what use cases should I use one over the other?

  • John5342
    John5342 over 10 years
    You should add that the future returned by async blocks on destruction (as if you called get) whereas the one returned from packaged_task does not.
  • Christian Rau
    Christian Rau over 10 years
    In the end a std::packaged_task is just a lower level feature for implementing std::async (which is why it can do more than std::async if used together with other lower level stuff, like std::thread). Simply spoken a std::packaged_task is a std::function linked to a std::future and std::async wraps and calls a std::packaged_task (possibly in a different thread).
  • Frank Liu
    Frank Liu over 9 years
    I am doing some experiments on the ~future() block. I could not replicate the blocking effect on the future object destruction. Everything worked asynchronously. I am using VS 2013 and when i launch the async, I used std::launch::async. Does VC++ somehow "fixed" this issue?
  • Zeta
    Zeta over 9 years
    @FrankLiu: Well, N3451 is a an accepted proposal, which (as far as I know) went into C++14. Given that Herb works at Microsoft, I wouldn't be surprised if that feature is implemented in VS2013. A compiler that strictly follows the C++11 rules would still show this behaviour.
  • Mikhail
    Mikhail almost 6 years
    "in C++14 this destructor will not block in the calling thread" - I believe this is not what C++17 standard says. See 33.6.5.5.3 (referenced from 33.6.7.9, describing effects of ~future()): "these actions will not block for the shared state to become ready, except that it may block if all of the following are true: the shared state was created by a call to std::async, the shared state is not yet ready, and this was the last reference to the shared state"
  • Zeta
    Zeta almost 6 years
    @Mikhail This answer precedes both C++14 and C++17, so I didn't have the standards but only proposals at hand. I'll remove the paragraph.
  • Christian Vincenzo Traina
    Christian Vincenzo Traina over 3 years
    I'd also add that std::packaged_task is the perfect fit to implement a thread pool queue