packaged_task 和 async 有啥区别

Posted

技术标签:

【中文标题】packaged_task 和 async 有啥区别【英文标题】:What is the difference between packaged_task and asyncpackaged_task 和 async 有什么区别 【发布时间】:2013-08-09 09:34:55 【问题描述】:

在使用 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::asyncstd::launch::deferred 可能会有很大的不同,但在这种情况下是否有一个?

这两种方法有什么区别,更重要的是,我应该在哪些用例中使用其中一种?

【问题讨论】:

【参考方案1】:

实际上你刚才给出的例子显示了如果你使用一个相当长的函数,比如

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

打包任务

packaged_task 不会自行启动,您必须调用它:

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 阻塞:

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 的文章 async and ~future 和描述见解的 Scott Meyer 的 std::futures from std::async aren't special。还要注意这种行为was specified in C++14 and up,但也通常在 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);

TL;DR

如果您想完成某些事情并且不在乎何时完成,请使用std::async,如果您想结束事情以便将它们移动到其他线程或稍后调用它们,请使用std::packaged_task。或者,引用Christian:

最后,std::packaged_task 只是实现std::async 的较低级别功能(这就是为什么如果与其他较低级别的东西一起使用,它可以做的比std::async 更多,例如std::thread)。简单地说,std::packaged_task 是一个链接到 std::futurestd::functionstd::async 包装并调用 std::packaged_task(可能在不同的线程中)。

【讨论】:

您应该添加 async 块在销毁时返回的未来(就像您调用 get 一样),而从 packaged_task 返回的则没有。 最后,std::packaged_task 只是实现std::async 的较低级别功能(这就是为什么如果与其他较低级别的东西一起使用,它可以做的比std::async 更多,例如@987654361 @)。 简单地说std::packaged_task是一个std::function链接到std::futurestd::async包装并调用std::packaged_task(可能在不同的线程中)。 我正在对 ~future() 块做一些实验。我无法复制对未来对象破坏的阻塞效果。一切都是异步工作的。我正在使用 VS 2013,当我启动异步时,我使用了 std::launch::async。 VC++ 是否以某种方式“修复”了这个问题? @FrankLiu:嗯,N3451 是一个被接受的提议,(据我所知)它进入了 C++14。鉴于 Herb 在 Microsoft 工作,如果该功能在 VS2013 中实现,我不会感到惊讶。严格遵循 C++11 规则的编译器仍会显示此行为。 @Mikhail 这个答案先于 C++14 和 C++17,所以我没有标准,只有手头的建议。我将删除该段落。【参考方案2】:

TL;DR

std::packaged_task 允许我们将std::future“绑定”到某个callable,然后控制何时何地执行这个callable,而不需要那个未来的对象。

std::async 启用第一个,但不启用第二个。也就是说,它允许我们获得一些可调用对象的未来,但是如果没有这个未来对象,我们就无法控制它的执行。

实际例子

下面是一个实际的问题示例,该问题可以使用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() 中执行调用。请注意,您不能为同一个共享状态创建多个期货(期货不可复制)。

【讨论】:

【参考方案3】:

打包任务与异步

p> 打包的任务包含一个任务[function or function object] 和未来/承诺对。当任务执行 return 语句时,它会在 packaged_task 的 promise 上引发 set_value(..)

a> 给定 Future、promise 和打包任务,我们可以创建简单的任务而无需过多担心线程[线程只是我们为运行任务而提供的东西]。

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

【讨论】:

【参考方案4】:

"类模板 std::packaged_task 包装了任何可调用的目标 (函数、lambda 表达式、绑定表达式或其他函数 object) 以便可以异步调用它。它的返回值或 抛出的异常存储在可以访问的共享状态中 通过 std::future 对象。”

"模板函数async异步运行函数f (可能在一个单独的线程中)并返回一个 std::future 将 最终保存该函数调用的结果。”

【讨论】:

以上是关于packaged_task 和 async 有啥区别的主要内容,如果未能解决你的问题,请参考以下文章

并非所有 std::packaged_task 在 std::async 调用中执行

何时在 async 或 packaged_task 上使用 promise?

Java 中 给一个object 赋值属性, 既可以用构造函数的方式,也可以用setXXXX()的方式,而它们之间有啥区

何时更喜欢 lambda 而不是带有 std::async 的打包任务? [复制]

Dart 中的 async 和 async* 有啥区别?

Angular 测试中的 fakeAsync 和 async 有啥区别?