当前位置: 首页 > 知识库问答 >
问题:

为什么std::future从std::packaged_task和std::a同步返回不同?

林念
2023-03-14

我知道了从< code>std::async返回的< code>future具有某种特殊共享状态的原因,通过这种状态,< code >等待返回的future发生在future的析构函数中。但是当我们使用< code>std::pakaged_task时,它的未来不会表现出同样的行为。要完成打包的任务,必须从< code>packaged_task显式调用< code>future对象上的< code>get()。

现在我的问题是:

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

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

它不会等待完成倒计时任务。但是,如果我取消注释 // int value = ret.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

  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在另一个线程上执行任务倒计时,无论我是否在返回的未来对象上使用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;
    }

共有3个答案

拓拔意
2023-03-14

行为的变化是由于<code>std::thread</code>和<code>std::async</code>之间的差异造成的。

在第一个示例中,您已经通过分离创建了一个守护线程。在这里打印< code>std::cout

在第二个示例中,您使用std::启动::deferred策略启动线程函数。std::a同步的行为是:

如果选择异步策略,相关的线程完成将与等待共享状态的第一个函数的成功返回同步,或者与释放共享状态的最后一个函数的返回同步,无论哪一个先出现。

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

这是Scott Meyers的一篇很棒的博文,阐明了< code>std::future的行为

相关SO帖子。

祁增
2023-03-14

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

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

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

暂时忘掉std::packaged_taskstd::futurestd::async,让我们后退一步,重新审视一下std::函数的工作方式以及它会导致什么问题。

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::p ackaged_task 所解决的问题。

特别是,它在线程之间创建一个通道,将结果发送给另一个线程。除此之外,它或多或少与< code>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::packated_tash是多线程友好的,因为它提供了一个通道,可以将结果传递给其他线程。它们都不会自己执行包装的可调用实体。需要在同一个线程或另一个线程中调用它们,以执行包装的可调用实体。所以在这个空间里基本上有两种东西:

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

这是一件不同的事情,因为它结合了执行的内容和执行的方式/地点。

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

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

希望这能帮助你了解幕后的情况。请参阅在线演示。

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

  • 一种类型可以通过 std::async 创建。这样的未来有一个相关的执行者,因此可以完成。
  • 其他类型可以通过std::p ackaged_task或类似的东西来创建。这种未来不一定有相关的遗嘱执行人,因此可能会或可能不会完成。

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

 {
   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.

希望这个答案能帮助你从不同的角度理解事情。

巩衡
2023-03-14

std::async 对如何以及在何处执行任务有明确的了解。这就是它的工作:执行任务。要做到这一点,它必须把它放在某个地方。某个地方可能是html" target="_blank">线程池,新创建的线程,或者在破坏未来的任何人执行的地方。

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

但是packaged_task没有。packaged_task所做的只是存储一个可以使用给定参数调用的可调用对象,创建一个带有函数返回值类型的Promise,并提供一种方法来获取未来和执行生成值的函数。

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

假设您要在新创建的线程上执行任务。好了,为了将其执行与未来的破坏同步,您需要一个互斥体,析构函数将阻止该互斥体,直到任务线程完成。

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

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

请注意,这不是< code>packaged_task所独有的。从用户创建的< code>promise对象创建的所有< code>future将不具有< code>async的< code>future的特殊属性

因此,问题确实应该是为什么异步以这种方式工作,而不是为什么其他人不这样做。

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

 类似资料:
  • 并行开发挺复杂的,特别是在试图用好线程和锁的过程中。如果要用到条件变量或std-atomics(一种无锁开发方式),那就更复杂了。C++0x提供了future和promise来简化任务线程间的返回值操作;同时为启动任务线程提供了packaged_task以方便操作。其中的关键点是允许2个任务间使用无(显式)锁的方式进行值传递;标准库帮你高效的做好这些了。基本思路很简单:当一个任务需要向父线程(启动

  • https://godbolt.org/z/P97MaK 我玩的概念和预期d::is_equality_comparable工作矢量,但它没有。 编译错误在 内部失败,而不是在受概念保护的函数边界处失败。 这是错误还是预期行为?

  • 问题内容: 我尝试将matlab代码转换为numpy,并发现numpy与std函数的结果不同。 在matlab中 在numpy中 这正常吗?我应该如何处理呢? 问题答案: NumPy函数采用一个可选参数:“自由度增量”。默认情况下是。对其进行设置以获取MATLAB结果: 要添加更多上下文,在计算方差(标准偏差为平方根)时,通常将其除以我们拥有的值的数量。 但是,如果我们从较大的分布中选择元素的随机

  • 在过去的几个月里,我一直在学习C语言并使用终端。我的代码使用g和c11编译并运行得很好,但在过去几天里它开始出现错误,此后我在编译时遇到了问题。我唯一可以编译和运行的程序依赖于旧的C标准。 我第一次遇到的错误包括 尝试使用ecg$g-o stoi_试验stoi_试验编译。cpp-std=c 11 大堆cpp:13:22:错误:命名空间“std”中没有名为“stoi”的成员;你是说“阿托伊”吗?in

  • 标准库函数bind()和function()定义于头文件<functional>中(该头文件还包括许多其他函数对象),用于处理函数及函数参数。bind()接受一个函数(或者函数对象,或者任何你可以通过”(…)”符号调用的事物),生成一个其有某一个或多个函数参数被“绑定”或重新组织的函数对象。(译注:顾名思义,bind()函数的意义就像它的函数名一样,是用来绑定函数调用的某些参数的。)例如: int