重新分配给尚未准备好的未来时会发生什么

What happens when reassigning to a future that is not ready yet

在代码审查期间,我遇到了一段代码,基本上可以归结为:

#include <iostream>
#include <future>
#include <thread>

int main( int, char ** )
{
    std::atomic<int> x( 0 );
    std::future<void> task;
    for( std::size_t i = 0u; i < 5u; ++i )
    {
        task = std::async( std::launch::async, [&x, i](){
                std::this_thread::sleep_for( std::chrono::seconds( 2u * ( 5u - i ) ) );
                ++x;
            } );
    }

    task.get();
    std::cout << x << std::endl;
    return 0;
}

我不太确定是否

我无法通过阅读互联网上的文档来回答这个问题,所以我想我会写上面的代码片段来找出我们的编译器实际上做了什么。

现在,我发现 gcc-5 所做的事情的答案是优柔寡断的,这让我更加好奇:人们会假设赋值是阻塞的还是非阻塞的。

如果是阻塞的话,程序所花费的时间基本上应该是各个任务执行所花费时间的总和。第一个用时 10 秒,第二个 8 秒,第三个 6 秒,第四个 4 秒,最后 2 秒。所以总共需要 10+8+6+4+2 = 30 秒.

如果是非阻塞的,它应该和最后一个任务一样长,即2秒

情况如下:需要 18 秒(使用时间 ./a.out 或一个好的旧时钟测量)。通过稍微研究一下代码,我发现代码的行为就好像赋值将交替阻塞和非阻塞。

但这不可能是真的,对吧? std::async 可能有一半的时间回落到 std::deferred?我的调试器说它产生了两个线程,阻塞直到两个线程退出,然后再产生两个线程等等。

标准是怎么说的?应该发生什么? gcc-5 内部发生了什么?

一般来说,通过 operator=(&&)task 的赋值不一定是阻塞的(见下文),但由于您使用 std::async 创建了 std::future,这些分配变得阻塞(感谢@T.C。):

[future.async]

If the implementation chooses the launch::async policy,

  • [...]

  • the associated thread completion synchronizes with ([intro.multithread]) the return from the first function that successfully detects the ready status of the shared state or with the return from the last function that releases the shared state, whichever happens first.

为什么执行时间为 18 秒?

你的情况是 std::async 在 赋值之前 为你的 lambda 启动 "thread" — 请参阅下面的详细说明获得 18 秒的执行时间。

这就是您的代码中(可能)发生的情况(e 代表一个 epsilon):

  • t = 0,首先 std::async 调用 i = 0,开始一个新线程;
  • t = 0 + e,第二个 std::async 调用 i = 1 开始一个新线程,然后移动。移动会释放task的当前共享状态,阻塞约10秒(但第二个std::asynci = 1已经在执行);
  • t = 10,第三次 std::async 调用 i = 2 开始一个新线程,然后移动。 task 的当前 共享状态 是与 i = 1 的调用,它已经准备就绪,因此没有任何阻塞;
  • t = 10 + e,第四次 std::async 调用 i = 3 开始一个新线程,然后移动。移动是阻塞的,因为前一个 std::asynci = 2 还没有准备好,但是 i = 3 的线程已经启动;
  • t = 16,第五次 std::async 调用 i = 4 开始一个新线程,然后移动。 taski = 3)的当前共享状态已经就绪,所以是非阻塞的;
  • t = 16 + e,跳出循环,调用 .get() 等待 *shared state` 就绪;
  • t = 18共享状态 准备就绪,所有内容结束。

关于 std::future::operator= 的标准详细信息:

这是 std::futureoperator= 的标准报价:

future& operator=(future&& rhs) noexcept;

Effects:

  • (10.1) — releases any shared state (30.6.4).
  • ...

这里是 "Releases any shared state" 的意思(重点是我的):

When an asynchronous return object or an asynchronous provider is said to release its shared state, it means:

(5.1) — [...]

(5.2) — [...]

(5.3) — these actions will not block for the shared state to become ready, except that it may block if all of the following are true: the shared state was created by a call to std::async, the shared state is not yet ready, and this was the last reference to the shared state.

你的情况属于我强调的(我认为)。您使用 std::async 创建了共享状态,它正在休眠(因此未就绪)并且您只有一个对它的引用,因此此 可能 正在阻塞。

it is guaranteed that all tasks are executed when printing out the result,

只有最后分配的任务才能保证被执行。至少,我找不到任何可以保证其余部分的规则。

whether the tasks would be executed one after another (i.e. the task assignment would be blocking) or not.

任务分配通常是非阻塞的,但在这种情况下它可能会阻塞 - 无法保证。

[futures.unique_future]

future& operator=(future&& rhs) noexcept;

  1. Effects:

    releases any shared state ([futures.state]).

[futures.state]

  1. When an asynchronous return object or an asynchronous provider is said to release its shared state, it means:

    • if the return object or provider holds the last reference to its shared state, the shared state is destroyed; and

    • the return object or provider gives up its reference to its shared state; and

    • these actions will not block for the shared state to become ready, except that it may block if all of the following are true: the shared state was created by a call to std::async, the shared state is not yet ready, and this was the last reference to the shared state.

std::async 创建的任务的所有潜在阻塞条件都尚未执行。