通过std::async
执行的函数的参数似乎共享未来的生命周期:
#include <iostream>
#include <future>
#include <thread>
struct S
{
S() {
std::cout << "S() " << (uintptr_t)this << std::endl;
}
S(S&& s) {
std::cout << "S(&&) " << (uintptr_t)this << std::endl;
}
S(const S& s) = delete;
~S() {
std::cout << "~S() " << (uintptr_t)this << std::endl;
}
};
int main()
{
{
std::cout << "enter scope" << std::endl;
auto func = [](S&& s) {
std::cout << "func " << (uintptr_t)&s << std::endl;
auto x = S();
};
S s;
auto fut = std::async(std::launch::async, func, std::move(s));
std::cout << "wait" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(5));
fut.get();
std::cout << "exit scope" << std::endl;
}
return 0;
}
结果:
enter scope
++S() 138054661364 main's variable
| S(&&) 138054661108 ++ std::async's internal copy
+--+S(&&) 138054659668 | std::async's internal copy
| | S(&&) 138054922824 +--+ func's argument
+--+~S() 138054659668 | |
| ~S() 138054661108 ++ |
| func 138054922824 |
| S() 138057733700 + | local variable
| ~S() 138057733700 + |
| wait |
| exit scope |
| ~S() 138054922824 +--+
++~S() 138054661364
看起来底层实现(MSVS 2015 U3)在地址138054922824
创建参数的最终版本,但在将来销毁之前不会销毁它。
感觉这会破坏RAII的承诺,因为函数实现可能会在退出时调用的参数的析构函数中继。
这是一个错误还是传递给std::async
的参数的确切生命周期未知?标准对此有何看法?
答案 0 :(得分:3)
在我之前的评论后面加上实际答案...
我在libstdc ++中遇到了相同的行为。我没有想到这种行为,并且在我的代码中导致了死锁错误(令人遗憾的是,由于等待超时,这只会导致程序终止的延迟)。在这种情况下,仅在将来销毁任务后,才销毁任务对象(我的意思是功能对象f
),但是任务对象和该实现对所有参数的处理方式相同。
std::async
的行为在[futures.async]中已标准化。
(3.1)如果在策略中设置了
launch::async
,则调用INVOKE(DECAY_COPY(std::forward<F>(f)), DECAY_COPY(std::forward<Args>(args))...)
([func.require],[thread.thread.constr])就像在一个线程代表的新执行线程中一样在调用DECAY_COPY()
的线程中评估对async
的调用的对象。任何返回值都将作为结果存储在共享状态中。从INVOKE(DECAY_COPY(std::forward<F>(f)), DECAY_COPY(std::forward<Args>(args))...)
执行中传播的任何异常都作为异常结果存储在共享状态中。线程对象以共享状态存储,并且会影响引用该状态的所有异步返回对象的行为。
通过使用DECAY_COPY
而不用在结果中命名并在INVOKE
表达式中使用措辞,确实强烈建议使用临时对象,这些临时对象在包含{{1 }},它发生在新的执行线程上。但是,这不足以得出参数(的副本)没有比清除函数调用更长的处理时间(或任何“合理的延迟”)超过函数调用的结论。其理由如下:基本上,标准要求在执行线程完成时销毁对象。但是,该标准不要求在执行等待调用或销毁未来之前,执行线程必须完成:
如果实现选择了
INVOKE
策略,(5.3) 共享此异步调用创建的共享状态的异步返回对象上的等待函数的调用应阻塞,直到相关联的线程完成(好像已加入),否则超时([thread.thread.member]); >
因此,等待的调用可以导致线程完成,然后才等待其完成。在假设规则下,如果代码仅表现出这种行为,则它们实际上可能会做得更糟,例如,将任务和/或参数公然地存储在共享状态下(要立即注意)。 IMO,这确实是一个漏洞。
libstdc ++的行为使得即使无条件的launch::async
也不足以导致任务和参数被破坏–仅wait()
或对未来的破坏。如果调用get()
,则仅破坏share()
的所有副本就足以导致破坏。实际上,这似乎是一个错误,因为shared_future
在(5.3)中的术语“等待功能”中确实包含,并且不能超时。除此之外,此行为似乎是未指定的–不管是不是疏忽。
我对为什么实现似乎将对象置于共享状态的猜测是,这比标准的字面意义(在目标线程上制作临时副本,与调用{{1 }}。
似乎应该提出LWG问题。不幸的是,对此的任何修复都可能破坏多个实现的ABI,因此即使在更改被批准的情况下,在行为中可靠地解决该问题也可能需要花费几年的时间。
我个人得出一个不幸的结论:wait()
有太多的设计和实现问题,以至于在非平凡的应用程序中几乎没有用。通过使用我自己的(依赖项跟踪)线程池类替换了std::async
令人反感的用法,我的代码中的上述错误已得到解决,这会在任务完成执行后尽快销毁包括所有捕获对象在内的任务。 (它只是从队列中弹出任务信息对象,其中包含类型擦除的任务,promise等)。
更新:应该注意,libstdc ++的std::async
具有相同的行为,任务std::async
处于std::packaged_task
时,似乎已进入共享状态,并且不会被销毁。 1}}或其他任何将来的析构函数都将挂起。
答案 1 :(得分:0)
行为实际上是正确的:S&&
是对std::async
创建的中间对象的引用,其生命周期等于返回的未来的生命周期。
<强>澄清强>
最初我误解了&&
是什么。我错过的是&&
只是一个参考,标准并不保证调用者会移动构造任何东西。调用者也可以将左值转换为右值引用。
预期流程:
fut
的构造函数move-constructs内部副本; fut
现在拥有s
fut
调用func
时,它会将另一个移动构建的副本作为右值传递; func
现在拥有s
func
退出s
后,实际流程:
fut
的构造函数move-constructs内部副本; fut
现在拥有s
fut
调用func
时,它会移动构建另一个内部副本,但会将其作为右值引用传递,而不是rvalue
; fut
仍然拥有s
func
退出s
后,func
不拥有任何内容正如Arne在他的回答中所解释的那样,标准确实允许这种行为。
一个简单的解决方法是top move-为每个rvalue引用参数构造一个本地副本(相对于func
的范围),其生命周期必须等于func
的生存期。