packaged_task和async之间的区别是什么



在使用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可能会有很大的不同,但在这种情况下有区别吗?

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

实际上,您刚才给出的示例显示了如果您使用相当长的函数(如)时的差异

//! 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 secondn";
// 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的文章async~future,其中描述了这个问题,以及Scott Meyer的std::async中的std::futures,它们并不特别,它们描述了这些见解。还要注意的是,这种行为在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);

TL;DR

如果你想完成一些事情,但并不真正关心它们何时完成,请使用std::async;如果你想结束一些事情,以便将它们转移到其他线程或稍后调用它们,请使用std::packaged_task。或者,引用克里斯蒂安的话:

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

TL;DR

CCD_ 29允许我们获得CCD_;有界的";到一些可调用,然后控制何时何地执行此可调用,而无需该未来对象。

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

实际示例

这里是一个可以用std::packaged_task解决但不能用std::async解决的问题的实际例子。

考虑您想要实现线程池。它由固定数量的工作线程共享队列组成。但共享的队列是什么?CCD_ 34在这里比较合适。

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(即使有延迟策略)并返回std::future,那么我们将无法选择如何在worker()中执行可调用的。请注意,不能为同一共享状态创建多个期货(期货不可复制)。

打包任务与异步

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

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

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

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

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

相关内容

最新更新