What is the difference between packaged_task and async
Asked Answered
H

4

187

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?

Hosea answered 9/8, 2013 at 9:34 Comment(0)
C
220

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 its 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).

Cowled answered 9/8, 2013 at 9:44 Comment(7)
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.Urian
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).Name
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?Brennen
@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.Cowled
"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"Aback
@Aback 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.Cowled
I'd also add that std::packaged_task is the perfect fit to implement a thread pool queueGalloon
W
11

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).

Weathers answered 12/11, 2021 at 9:3 Comment(4)
I believe that the statement that such functionality is impossible to implement with std::async is not entirely correct. We can use the std::launch::deferred policy which effectively makes std::async behave very similar to std::packaged_task if I'm not mistaken.Robles
@Robles No, that's not true. With the deferred policy, you still need the future object to (then synchronously) execute the task. A packaged task can be executed independently of the corresponding future.Weathers
I agree on that, but one can implement such a thread pool as you did above by having a queue of futures and creating them from std::function objects through the std::async tasks having the std::launch::deferred policy. This way we can still perform the actual work form the worker thread. We would only have to share the future with the outside then. I think using std::packaged_task is better for this but just wanted to point out that it is possible to implement a similar thing with the std::async.Robles
@Robles Allright, yes, this would be possible. std::shared_future might be a suitable tool.Weathers
R
1

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 .

Reverend answered 20/10, 2015 at 14:1 Comment(0)
A
0

"The class template std::packaged_task wraps any callable target (function, lambda expression, bind expression, or another function object) so that it can be invoked asynchronously. Its return value or exception thrown is stored in a shared state which can be accessed through std::future objects."

"The template function async runs the function f asynchronously (potentially in a separate thread) and returns a std::future that will eventually hold the result of that function call."

Auld answered 9/8, 2013 at 9:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.