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

gau*_*waj 7 c++ packaged-task stdasync c++14 std-future

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

现在我的问题是:

  1. 未来(思考std::asyncstd::packaged_task)的内部实现可能是什么?
  2. 为什么相同的行为不适用于futurereturn from std::packaged_task?或者,换句话说,相同的行为是如何停止的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;
}
Run Code Online (Sandbox Code Playgroud)

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

// 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;
    }
Run Code Online (Sandbox Code Playgroud)

Nic*_*las 5

std::async对所给任务的执行方式和地点有明确的了解。那就是它的工作:执行任务。要做到这一点,它实际上必须把它放在某个地方。某个地方可能是一个线程池、一个新创建的线程,或者在一个由破坏future.

因为async知道函数将如何执行,它拥有构建一种机制所需的 100% 信息,该机制可以在潜在的异步执行结束时进行通信,并确保如果您破坏了future,那么无论将要执行的机制执行该函数最终将开始实际执行它。毕竟,它知道那个机制是什么。

packaged_task 没有。所有packaged_task不为存储可以用给定的参数来调用一个调用对象,创建一个promise与函数的返回值的类型,并提供两种手段获得future和执行产生的价值功能。

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

假设您想在新创建的线程上执行任务。好的,因此要将其执行与future的析构同步,您需要一个互斥锁,析构函数将在该互斥锁上阻塞,直到任务线程完成。

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

因为packaged_task不知道您打算如何执行它,所以它无法执行任何操作。

请注意,这不是packaged_task. 从用户创建的对象创建的所有 futurespromise都不会具有asyncsfuture的特殊属性。

所以问题真的应该是为什么这样async工作,而不是为什么其他人不这样做

如果你想知道这一点,那是因为两个相互竞争的需求:async需要是一种高级的、脑残的简单方法来获得异步执行(对于销毁同步是有意义的),并且没有人想要创建一个新的future除了其析构函数的行为外,类型与现有类型相同。因此,他们决定重载future工作原理,使其实现和使用复杂化。


Naw*_*waz 5

@Nicol Bolas已经非常满意地回答了这个问题。因此,我将尝试从不同的角度稍微回答这个问题,详细阐述@Nicol Bolas 已经提到的要点。

相关事物的设计及其目标

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

int add(int a, int b) {
    std::cout << "adding: " << a << ", "<< b << std::endl;
    return a + b;
}
Run Code Online (Sandbox Code Playgroud)

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

情况 1 — std::function不足以在不同线程中执行事物

std::function<int(int,int)> f { add };
Run Code Online (Sandbox Code Playgroud)

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

int result = f(1, 2); //note we can get the result here
Run Code Online (Sandbox Code Playgroud)

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

std::thread t { std::move(f), 3, 4 };
t.join(); 
Run Code Online (Sandbox Code Playgroud)

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

case 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
Run Code Online (Sandbox Code Playgroud)

现在您将看到如何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
Run Code Online (Sandbox Code Playgroud)

所以,从根本上std::functionstd::packaged_task类似这样的事情:他们只是包装调用实体,有一点不同:std::packaged_task在支持多线程的,因为它提供了通过它可以将结果传递给其他线程的通道。它们都不会自己执行包装的可调用实体。需要在同一个线程或另一个线程中调用它们来执行包装的可调用实体。所以在这个空间里基本上有两种东西:

  • 执行的是什么,即常规函数std::functionstd::packaged_task、 等。
  • 如何/在哪里执行,即线程、线程池、执行器等。

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

这是另一回事,因为它将what-is-executedhow/where-is-executed 结合起来

std::future<int> fut = std::async(add, 100, 200);
int result = fut.get();
Run Code Online (Sandbox Code Playgroud)

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

希望能帮助您了解幕后的工作原理。请参阅在线演示

两种类型的区别 std::future

好吧,在这一点上,很明显std::future可以创建两种类型:

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

因为,在第二种情况下,future 不一定有关联的 executor,它的析构函数不是为完成/等待而设计的,因为它可能永远不会完成:

 {
   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.
Run Code Online (Sandbox Code Playgroud)

希望这个答案可以帮助您从不同的角度理解事物。