为什么 std::future 与 std::packaged_task 和 std::async 返回的不同?

Why std::future is different returned from std::packaged_task and std::async?

我知道了从 std::async 返回的 future 有一些特殊的共享状态的原因,通过这些共享状态 wait on returned future 发生在 future 的析构函数中。但是当我们使用 std::pakaged_task 时,它的未来不会表现出相同的行为。 要完成打包任务,您必须从 packaged_task.

显式调用 future 对象上的 get()

现在我的问题是:

  1. future 的内部实现是什么(思考 std::async vs std::packaged_task)?
  2. 为什么相同的行为没有应用于从 std::packaged_task 返回的 future?或者,换句话说,std::packaged_task future 的相同行为是如何停止的?

要查看上下文,请看下面的代码:

它不会等待完成 countdown 任务。但是,如果我取消注释 // int value = ret.get();,它将完成 countdown 并且很明显,因为我们实际上是在阻止返回的未来。

    // packaged_task example
#include <iostream>     // std::cout
#include <future>       // std::packaged_task, std::future
#include <chrono>       // std::chrono::seconds
#include <thread>       // std::thread, std::this_thread::sleep_for

// count down taking a second for each value:
int countdown (int from, int to) {
  for (int i=from; i!=to; --i) {
    std::cout << i << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
  }
  std::cout << "Lift off!" <<std::endl;
  return from-to;
}

int main ()
{
   std::cout << "Start " << std::endl;
  std::packaged_task<int(int,int)> tsk (countdown);   // set up packaged_task
  std::future<int> ret = tsk.get_future();            // get future

  std::thread th (std::move(tsk),10,0);   // spawn thread to count down from 10 to 0

//   int value = ret.get();                  // wait for the task to finish and get result

  std::cout << "The countdown lasted for " << std::endl;//<< value << " seconds.\n";

  th.detach();   

  return 0;
}

如果我使用 std::async 在另一个线程上执行任务 countdown,无论我是在返回的 future 对象上使用 get() 还是 ,它总是会完成任务。

// packaged_task example
#include <iostream>     // std::cout
#include <future>       // std::packaged_task, std::future
#include <chrono>       // std::chrono::seconds
#include <thread>       // std::thread, std::this_thread::sleep_for

    // count down taking a second for each value:
    int countdown (int from, int to) {
      for (int i=from; i!=to; --i) {
        std::cout << i << std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(1));
      }
      std::cout << "Lift off!" <<std::endl;
      return from-to;
    }
    
    int main ()
    {
       std::cout << "Start " << std::endl;
      std::packaged_task<int(int,int)> tsk (countdown);   // set up packaged_task
      std::future<int> ret = tsk.get_future();            // get future
    
      auto fut = std::async(std::move(tsk), 10, 0);   

    
    //   int value = fut.get();                  // wait for the task to finish and get result
    
      std::cout << "The countdown lasted for " << std::endl;//<< value << " seconds.\n";

      return 0;
    }

行为的变化是由于 std::threadstd::async 之间的差异。

在第一个示例中,您通过分离创建了一个守护线程。在主线程中打印 std::cout << "The countdown lasted for " << std::endl; 的位置可能会在 countdown 线程函数内的打印语句之前、期间或之后发生。因为主线程不等待生成的线程,您甚至可能看不到所有的打印输出。

在第二个示例中,您使用 std::launch::deferred 策略启动线程函数。 behaviour for std::async 是:

If the async policy is chosen, the associated thread completion synchronizes-with the successful return from the first function that is waiting on the shared state, or with the return of the last function that releases the shared state, whichever comes first.

在这个例子中,你有两个相同共享状态的未来。在退出 main 时调用它们的 dtor 之前,异步任务必须完成。即使您没有明确定义任何未来,创建和销毁的临时未来(从对 std::async 的调用返回)也意味着任务在主线程退出之前完成。


Here 是 Scott Meyers 的精彩博客 post,阐明了 std::future & std::async.

的行为

.

std::async 明确知道任务的执行方式和位置。这就是它的工作:执行任务。为此,它实际上必须将其放在某个地方。某个地方可能是一个线程池,一个新创建的线程,或者在一个由任何破坏 future.

的人执行的地方

因为 async 知道函数将如何执行,它拥有构建机制所需的 100% 信息,该机制可以在潜在的异步执行结束时进行通信,并确保如果你销毁了 future,那么任何将要执行该函数的机制最终都会绕过实际执行它。毕竟,它知道那个机制是什么。

但是packaged_task 不会packaged_task 所做的只是存储一个可以用给定参数调用的可调用对象,创建一个具有函数 return 值类型的 promise,并提供一种方法来获得 future 并执行生成值的函数。

任务实际执行的时间和地点是 none packaged_task 的事。没有这些知识,就无法构建使 future 的析构函数与任务同步所需的同步。

假设您要在 freshly-created 线程上执行任务。好的,因此要使其执行与 future 的销毁同步,您需要一个互斥量,析构函数将阻塞该互斥量直到任务线程完成。

但是如果您想在与 future 的析构函数调用者相同的线程中执行任务怎么办?好吧,那么你 不能 使用互斥锁来同步它,因为它们都在同一个线程上。相反,您需要让析构函数调用任务。这是一个完全不同的机制,它取决于您计划如何执行。

因为packaged_task不知道你打算如何执行它,所以它不能做任何事情。

请注意,这并非 packaged_task 所独有。从 user-created promise 对象创建的 All future 将没有 async 的特殊 属性 futures.

所以真正的问题应该是为什么 async 以这种方式工作,而不是为什么其他人 .

如果您想知道这一点,那是因为两个相互竞争的需求:async 需要成为一种 high-level、brain-dead 获得异步执行的简单方法(为此 sychronization-on-destruction 是有道理的),而且没有人愿意创建一个新的 future 类型,除了其析构函数的行为外,它与现有类型完全相同。因此,他们决定重载 future 的工作方式,使其实现和使用变得复杂。

@Nicol Bolas 对这个问题 相当满意。所以我会尝试从不同的角度稍微回答这个问题,详细说明@Nicol Bolas 已经提到的要点。

相关事物的设计及其目标

考虑我们要以各种方式执行的这个简单函数:

int add(int a, int b) {
    std::cout << "adding: " << a << ", "<< b << std::endl;
    return a + b;
}

暂时忘记std::packaged_taskstd ::futurestd::async,让我们退后一步,重新审视std::function的工作原理以及问题 它导致。

案例 1 — std::function 不足以在不同线程中执行任务

std::function<int(int,int)> f { add };

一旦我们有了 f,我们就可以在同一个线程中执行它,例如:

int result = f(1, 2); //note we can get the result here

或者,在不同的线程中,像这样:

std::thread t { std::move(f), 3, 4 };
t.join(); 

如果仔细观察,我们会发现在不同的线程中执行 f 会产生一个新问题:我们如何获取函数的结果? 执行 f 在同一个线程中没有那个问题——我们得到的结果作为返回值,但是当在不同的线程中执行时,我们没有任何办法得到结果。这正是 std::packaged_task.

解决的问题

案例2——std::packaged_task解决了std::function没有解决的问题

特别是,它在线程之间创建一个通道以将结果发送到另一个线程。除此之外,它或多或少与 std::function.

相同
std::packaged_task<int(int,int)> f { add }; // almost same as before

std::future<int> channel = f.get_future();  // get the channel
    
std::thread t{ std::move(f), 30, 40 }; // same as before
t.join();  // same as before
    
int result = channel.get(); // problem solved: get the result from the channel

现在您可以看到 std::packaged_task 如何解决 std::function 造成的问题。然而,这并不意味着 std::packaged_task 必须在不同的线程中执行。您也可以在同一个线程中执行它,就像 std::function 一样,尽管您仍然会从通道中获得结果。

std::packaged_task<int(int,int)> f { add }; // same as before
std::future<int> channel = f.get_future(); // same as before
    
f(10, 20); // execute it in the current thread !!

int result = channel.get(); // same as before

所以从根本上说 std::functionstd::packaged_task 是类似的东西:它们只是包装可调用实体,有一个区别:std::packaged_task 是 multithreading-friendly,因为它提供了一个它可以将结果传递给其他线程的通道。它们都不会自己执行包装的可调用实体。需要在同一个线程或另一个线程中 调用 它们来执行包装的可调用实体。所以基本上这个 space:

有两种东西
  • 执行什么 即常规函数,std::functionstd::packaged_task
  • how/where 被执行 即线程、线程池、执行器等

情况 3:std::async 是完全不同的事情

这是不同的东西,因为它结合了what-is-executedhow/where-is-executed

std::future<int> fut = std::async(add, 100, 200);
int result = fut.get();

请注意,在这种情况下,创建的未来有一个关联的执行者,这意味着未来将在某个时候完成,因为有人在幕后执行事情。但是,对于 std::packaged_task 创建的未来,不一定有执行者,如果创建的任务 从未 给任何执行者,那么未来可能永远不会完成。

希望这能帮助您了解幕后工作原理。参见 the online demo

两种的区别std::future

嗯,在这一点上,很明显可以创建两种 std::future

  • std::async可以创建一种。这样的未来有一个关联的执行者,因此可以完成。
  • 其他类型可以由 std::packaged_task 或类似的东西创建。这样的未来不一定有关联的执行者,因此可能会或可能不会完成。

因为,在第二种情况下,未来不一定有关联的执行者,它的析构函数是 而不是 为它的 completion/wait 设计的,因为它可能永远不会完成:

 {
   std::packaged_task<int(int,int)> f { add };
 
   std::future<int> fut = f.get_future(); 

 } // fut goes out of scope, but there is no point 
   // in waiting in its destructor, as it cannot complete 
   // because as `f` is not given to any executor.

希望这个回答能帮助您从不同的角度理解事物。