packaged_task和async之间有什么区别?

183

在使用C++11的线程模型时,我注意到

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

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

看起来这两种方法做的事情完全相同。我知道如果我使用 std::launch::deferred 运行 std::async,可能会有一个重大区别,但在这种情况下是否有区别呢?

这两种方法之间的区别是什么,更重要的是,在什么用例下应该使用其中一种而不是另一种?

4个回答

215
实际上,你刚刚举的例子展示了使用较长函数时的差异,比如...
//! sleeps for one second and returns 1
auto sleep = [](){
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return 1;
};

封装任务

封装任务不会自动启动,您需要调用它:

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

另一方面,std::asynclaunch::async一起使用时,会尝试在不同的线程中运行任务:

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

缺点

但在你试图将async用于所有情况之前,请记住返回的future具有特殊的共享状态,这要求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;
*/

所以,如果你想要真正的异步操作,你需要保留返回的 "future",或者如果你不在意结果是否会改变的话:
{
    auto pizza = std::async(get_pizza);
    /* ... */
    if(need_to_go)
        return;          // ~future will block
    else
       eat(pizza.get());
}   

有关此事的更多信息,请参阅Herb Sutter的文章{{link1:async~future}},其中描述了问题,以及Scott Meyer的文章{{link2:std::futuresstd::async不是特殊的}},其中描述了一些见解。还请注意,这种行为在C++14及以上版本中已经规定了,但在C++11中也常见实现。

进一步的区别

使用std::async时,您无法再将任务运行在特定的线程上,而std::packaged_task可以移动到其他线程上运行。

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

此外,在调用 f.get() 之前,需要调用 packaged_task ,否则您的程序将会冻结,因为未来永远不会准备好。
std::packaged_task<int(int,int)> task(...);
auto f = task.get_future();
std::cout << f.get() << "\n"; // oops!
task(2,3);

简而言之

如果你想要完成一些事情,但并不在乎它们何时完成,那就使用std::async;如果你想要将事情封装起来以便将它们移动到其他线程或稍后调用它们,那就使用std::packaged_task。或者,引用Christian的话:

最终,std::packaged_task只是一个更低层次的功能,用于实现std::async(这就是为什么它可以在与其他更低层次的东西(如std::thread)一起使用时做更多事情)。简单来说,std::packaged_task是一个与std::futurestd::function相关联的东西,而std::async则封装并调用一个std::packaged_task(可能在不同的线程中)。


11
你应该补充说明,async块在销毁时返回未来对象的结果(就好像你调用了get一样),而packaged_task返回的未来对象却不会。 - John5342
27
最终,std::packaged_task 只是实现 std::async 的一种更低级别的特性(这就是为什么如果与其他低级别的东西一起使用,比如 std::thread,它可以做更多的事情)。简单来说,std::packaged_task 是一个链接到 std::futurestd::function,而 std::async 则包装并调用一个 std::packaged_task(可能在不同的线程中)。 - Christian Rau
我正在对 ~future() 块进行一些实验。我无法复制 future 对象销毁时的阻塞效果。一切都是异步工作的。我正在使用 VS 2013,当我启动异步操作时,我使用了 std::launch::async。VC++ 是否在某种程度上“修复”了这个问题? - Frank Liu
1
@FrankLiu:N3451是一个已被接受的提案,据我所知,该提案已经被纳入了C++14标准。考虑到Herb在微软工作,如果这个特性被实现到VS2013中,我并不会感到惊讶。一个严格遵循C++11规则的编译器仍然会显示这种行为。 - Zeta
1
@Mikhail 这个答案是在 C++14 和 C++17 出现之前发布的,所以我只有提案文档,没有标准文件。我会删除这一段。 - Zeta
显示剩余2条评论

10

简介

std::packaged_task 允许我们获取与某个 可调用对象 绑定的 std::future,并控制何时以及在哪里执行该可调用对象,而无需依赖于该 future 对象。

std::async 同样能够实现前者,但不包括后者。也就是说,它允许我们获取某个可调用对象的 future,但是没有 future 对象,我们就无法控制其执行。

实际示例

下面是一个可以使用 std::packaged_task 但不能使用 std::async 解决的实际问题示例。

假设你想要实现一个 线程池。它由固定数量的 工作线程 和一个 共享队列 组成。但共享什么队列呢?std::packaged_task 在这里非常适用。

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

使用std::async无法实现这样的功能。我们需要从enqueue()返回一个std::future。如果在那里调用std::async(即使是使用deferred策略)并返回std::future,那么我们将无法选择如何在worker()中执行可调用对象。请注意,您不能为相同共享状态创建多个std::futurestd::future是不可复制的)。


我相信说使用 std::async 实现这种功能是不可能的这个说法并不完全正确。我们可以使用 std::launch::deferred 策略,这将使 std::async 的行为非常类似于 std::packaged_task,如果我没有弄错的话。 - niosus
@niosus 不,那不是真的。使用延迟策略,你仍然需要 future 对象来(然后同步地)执行任务。而 packaged task 可以独立于相应的 future 执行。 - Daniel Langr
1
我同意这一点,但是可以通过拥有一个期望队列并通过使用std::launch::deferred策略从std::function对象创建它们来实现上述的线程池。这样我们仍然可以在工作线程中执行实际的工作。然后我们只需要与外部共享期望。我认为使用std::packaged_task更好,但只是想指出使用std::async也可以实现类似的功能。 - niosus
1
@niosus 好的,是的,这是可能的。std::shared_future 可能是一个合适的工具。 - Daniel Langr

1

Packaged Task vs async

p> Packaged task包含一个任务[函数或函数对象]和future/promise对。当任务执行返回语句时,它会在packaged_task的promise上调用set_value(..)

a>给定Future、promise和package task,我们可以创建简单的任务而不必过多担心线程[线程只是我们给予运行任务的一些东西]。

然而,我们需要考虑使用多少个线程,或者一个任务是否最好在当前线程上运行还是在另一个线程上运行等。这样的决策可以由一个称为async()的线程启动器处理,它决定是创建一个新的线程还是回收旧的线程,或者仅在当前线程上运行任务。它返回一个future。


0
"class template std::packaged_task可以包装任何可调用目标(函数、lambda表达式、绑定表达式或另一个函数对象),以便可以异步调用它。其返回值或抛出的异常存储在共享状态中,可以通过std::future对象访问。"
"模板函数async异步运行函数f(可能在单独的线程中),并返回一个std::future,最终将保存该函数调用的结果。"

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接