155

在使用 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但在这种情况下是否存在差异?

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

4

4 回答 4

183

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

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

打包任务

Apackaged_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::asyncwithlaunch::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~future,以及描述见解的 Scott Meyer 的std::futures文章并std::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";

此外,packaged_task在调用之前需要调用a f.get(),否则你的程序将冻结,因为未来永远不会准备好:

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

TL;博士

如果std::async您想要完成某些事情并且并不真正关心它们何时完成,并且std::packaged_task如果您想包装一些事情以便将它们移动到其他线程或稍后调用它们。或者,引用Christian的话:

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

于 2013-08-09T09:44:18.413 回答
3

TL;博士

std::packaged_task允许我们获得某个callablestd::future的“有界” ,然后控制何时何地执行这个 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. 我们需要返回一个std::futurefrom enqueue()。如果我们std::async在那里调用(即使使用延迟策略)并返回std::future,那么我们将无法选择如何执行可调用 in worker()。请注意,您不能为同一个共享状态创建多个期货(期货是不可复制的)。

于 2021-11-12T09:03:22.377 回答
1

打包任务与异步

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

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

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

于 2015-10-20T14:01:46.667 回答
0

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

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

于 2013-08-09T09:44:44.790 回答