为啥 std::future 从 std::packaged_task 和 std::async 返回不同?
Posted
技术标签:
【中文标题】为啥 std::future 从 std::packaged_task 和 std::async 返回不同?【英文标题】:Why std::future is different returned from std::packaged_task and std::async?为什么 std::future 从 std::packaged_task 和 std::async 返回不同? 【发布时间】:2020-12-29 18:39:15 【问题描述】:我知道了为什么从std::async
返回的future
有一些特殊的共享状态,wait on returned future
通过它在未来的析构函数中发生。但是当我们使用std::pakaged_task
时,它的未来不会表现出相同的行为。
要完成打包任务,您必须从 packaged_task
显式调用 future
对象上的 get()
。
现在我的问题是:
-
未来的内部实现可能是什么(思考
std::async
与std::packaged_task
)?
为什么相同的行为不适用于从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;
【问题讨论】:
在第一个程序中,您没有等待线程运行,主线程甚至在该线程启动之前就退出了。 @Ayub 在第二个节目中,我也不等了。但是程序本身在未来的析构函数中等待。这就是我关于未来行为差异的问题(来自程序 1 和程序 2) 您可以尝试在 std::async 中使用 std::launch::async(或 ...::deferred)。async
析构函数将等待它的未来完成只有如果它已经启动。
@olepinto 您的观察似乎是正确的,但它没有回答我的问题。
【参考方案1】:
行为的改变是由于std::thread
和std::async
之间的差异。
在第一个示例中,您通过分离创建了一个守护线程。在主线程中打印std::cout << "The countdown lasted for " << std::endl;
的位置可能发生在countdown
线程函数内的打印语句之前、期间或之后。因为主线程不会等待生成的线程,所以您甚至可能看不到所有的打印输出。
在第二个示例中,您使用std::launch::deferred
策略启动线程函数。 behaviour for std::async 是:
如果选择了异步策略,则关联的线程完成与第一个等待共享状态的函数的成功返回同步,或与释放共享状态的最后一个函数的返回同步,以先到者为准。
在此示例中,您有两个用于相同共享状态的期货。在退出 main 时调用它们的 dtor 之前,必须完成异步任务。即使您没有明确定义任何未来,被创建和销毁的临时未来(从对 std::async
的调用中返回)将意味着任务在主线程退出之前完成。
Here 是 Scott Meyers 的一篇很棒的博文,阐明了 std::future
和 std::async
的行为。
Related SO post.
【讨论】:
【参考方案2】:std::async
清楚知道如何以及在何处执行给定的任务。这就是它的工作:执行任务。要做到这一点,它必须把它放在某个地方。那个地方可能是一个线程池,一个新创建的线程,或者在一个由谁破坏future
来执行的地方。
因为async
知道函数将如何执行,所以它拥有 100% 所需的信息来构建一种可以在潜在异步执行结束时进行通信的机制,并确保如果您销毁 @ 987654324@,那么执行该函数的任何机制最终都会绕过实际执行它。毕竟,它知道那个机制是什么。
但是packaged_task
没有。 packaged_task
所做的只是存储一个可以使用给定参数调用的可调用对象,创建一个具有函数返回值类型的 promise
,并提供一种获取 future
和执行函数的方法生成值。
任务实际执行的时间和地点与packaged_task
无关。如果没有这些知识,就无法构建使future
的析构函数与任务同步所需的同步。
假设您想在新创建的线程上执行任务。好的,因此要将其执行与future
的销毁同步,您需要一个互斥锁,析构函数将阻塞该互斥锁,直到任务线程完成。
但是,如果您想在与future
的析构函数的调用者相同的线程中执行任务怎么办?好吧,那么你不能使用互斥锁来同步它,因为它都在同一个线程上。相反,您需要让析构函数调用该任务。这是一个完全不同的机制,它取决于你计划如何执行。
因为packaged_task
不知道您打算如何执行它,所以它无法执行任何操作。
请注意,这不是 packaged_task
独有的。 所有从用户创建的promise
对象创建的future
s 将不具有async
的future
s 的特殊属性。
所以问题应该是为什么async
会这样工作,而不是为什么其他人不这样做。
如果您想知道这一点,那是因为两个相互竞争的需求:async
需要是一种高级的、脑死的简单方法来获得异步执行(销毁时同步是有意义的),以及没有人想创建一个新的future
类型,除了析构函数的行为外,它与现有的类型相同。因此他们决定重载future
的工作方式,使其实现和使用变得复杂。
【讨论】:
【参考方案3】:@Nicol Bolas 对already answered 这个问题非常满意。因此,我将尝试从不同的角度稍微回答这个问题,详细说明@Nicol Bolas 已经提到的观点。
相关事物的设计及其目标
考虑我们想要以各种方式执行的这个简单函数:
int add(int a, int b)
std::cout << "adding: " << a << ", "<< b << std::endl;
return a + b;
暂时忘记std::packaged_task
、std ::future
和std::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::function
和std::packaged_task
是类似的东西:它们只是包装可调用实体,有一个区别:std::packaged_task
是多线程友好的,因为它提供了一个通道,通过它可以将结果传递给其他线程。他们俩都不自己执行包装的可调用实体。需要在同一个线程或另一个线程中调用它们,以执行包装的可调用实体。所以基本上这个空间里有两种东西:
std::function
、std::packaged_task
等。
如何/在何处执行,即线程、线程池、执行程序等。
案例 3:std::async
完全不同
这是另一回事,因为它结合了 what-is-executed 和 how/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
或类似的东西创建。这样的未来不一定有关联的执行者,因此可能会也可能不会完成。
因为在第二种情况下,future 不一定有关联的执行器,它的析构函数不是为它的完成/等待而设计的,因为它可能永远不会完成:
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.
希望这个答案可以帮助您从不同的角度理解事物。
【讨论】:
添加一些关于std::promise
的信息也会对潜在读者有所帮助。
@Ajay:答案已经太长了。所以我克制自己不要添加更多的东西。对于想了解std::promise
和std::future
如何协同工作的好奇读者,本主题的答案非常棒:***.com/questions/11004273/what-is-stdpromise .. 还有这个:***.com/questions/12620186/futures-vs-promises以上是关于为啥 std::future 从 std::packaged_task 和 std::async 返回不同?的主要内容,如果未能解决你的问题,请参考以下文章
在 c++11 中转换 std::future 或 std::shared_future