6

似乎通过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是未知的?标准对此有何评论?

4

2 回答 2

3

用一个实际的答案跟进我之前的评论......</p>

我遇到了与 libstdc++ 相同的行为。我没想到会出现这种行为,它导致我的代码出现死锁错误(幸运的是,由于等待超时,这只导致程序终止延迟)。在这种情况下,任务对象(我的意思是函数对象f)在任务完成执行后没有被销毁,只是在未来被销毁时,但是,任务对象和任何参数很可能被处理以同样的方式执行。

的行为在[futures.async]std::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表达式内部,强烈建议使用在包含 的完整表达式的末尾被销毁的临时对象INVOKE,这发生在新的执行线程上。但是,这还不足以断定参数(的副本)不会超过函数调用的时间超过清理它们所需的处理时间(或任何“合理的延迟”)。它的原因是这样的:基本上标准要求对象在执行线程完成时被销毁。但是,该标准不要求执行线程在等待调用或未来被破坏之前完成:

如果实现选择launch​::​async策略,

(5.3) 对共享此异步调用创建的共享状态的异步返回对象上的等待函数的调用应阻塞,直到相关线程完成,好像已加入,否则超时([thread.thread.member]) ;

因此,等待调用可能会导致线程完成,然后才等待其完成。在 as-if 规则下,如果代码只是看起来有这种行为,它们实际上可能会做更糟糕的事情,例如公然将任务和/或参数存储在共享状态(需要立即注意)。这似乎是一个漏洞,IMO。

libstdc++ 的行为是这样的,即使是无条件wait()的也不足以导致任务和参数被破坏——只有 a get()or 未来的破坏才会。如果share()被调用,只有销毁所有副本shared_future就足以导致销毁。这似乎确实是一个错误,正如wait()(5.3) 中的术语“等待函数”所涵盖的那样,并且不能超时。除此之外,这种行为似乎是未指定的——无论这是否是疏忽。

对于实现似乎将对象置于共享状态的原因,我的猜测是,这比标准字面上的建议(在目标线程上制作临时副本,与调用同步std::async)更容易实现。

似乎应该就此提出一个 LWG 问题。不幸的是,对此的任何修复都可能会破坏多个实现的 ABI,因此即使更改获得批准,也可能需要数年才能在部署中可靠地修复该行为。

就我个人而言,我得出了一个不幸的结论,即std::async存在如此多的设计和实现问题,以至于在非平凡的应用程序中几乎毫无用处。我的代码中的上述错误已通过我替换std::async使用我自己的(依赖跟踪)线程池类的违规使用来解决,该类在任务完成执行后尽快销毁包括所有捕获的对象的任务。(它只是简单地从队列中弹出任务信息对象,其中包含类型擦除的任务、承诺等。)

更新:应该注意,libstdc++std::packaged_task具有相同的行为,任务似乎已移至共享状态,并且不会在 is 时被销毁std::packaged_task,只要get()或任何未来的析构函数处于挂起状态。

于 2018-07-16T09:26:30.103 回答
0

行为实际上是正确的:S&&是对中间对象的引用,该对象std::async的生命周期等于返回的未来的生命周期。

澄清

最初我误解了什么&&是。我错过的是这&&只是一个参考,标准并不能保证调用者会移动构建任何东西。调用者也可以将左值转换为右值引用。

预期流量:

  1. fut的构造函数移动构造内部副本;fut现在拥有s
  2. fut调用func它时,它将另一个移动构造的副本作为右值传递;func现在拥有s
  3. onfunc的出口s被破坏

实际流量:

  1. fut的构造函数移动构造内部副本;fut现在拥有s
  2. fut调用func它时,移动构造另一个内部副本,但将其作为右值引用传递,而不是rvalue; fut 仍然拥有s
  3. 退出后func什么都没有发生s,因为func不拥有它

正如 Arne 在他的回答中解释的那样,标准确实允许这种行为。

func一个简单的解决方法是为每个生存期必须等于生存期的右值引用参数顶部移动构造一个本地副本(相对于的范围) func

于 2018-03-27T06:33:57.863 回答