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

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

本文关键字:未来 什么 准备好 未准备 新分配 分配      更新时间:2023-10-16

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

#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.(:

[未来.异步]

如果实现选择启动::异步策略,

  • [...]

  • 关联的线程完成与 ([intro.multithread]( 成功检测共享状态的就绪状态的第一个函数的返回同步,或与释放共享状态的最后一个函数的返回同步,以先发生者为准。

为什么你会得到 18 秒的执行时间?

在您的情况下,std::async在分配之前启动 lambda 的"线程" — 有关如何获得 18 秒执行时间的详细说明,请参阅下文。

这是(可能(在你的代码中发生的情况(e代表epsilon(:

  • t = 0,首先std::asynci = 0调用,开始一个新线程;
  • t = 0 + ei = 1启动新线程的第二个std::async调用,然后移动。此举将释放 task当前共享状态,阻塞约 10 秒(但带有 i = 1 的第二个std::async已经在执行(;
  • t = 10,第三次std::async调用,i = 2启动一个新线程,然后移动。task当前共享状态是与i = 1的调用,它已经准备就绪,因此没有任何阻塞;
  • t = 10 + e,第四std::async调用,i = 3启动一个新线程,然后移动。移动被阻止,因为之前的std::async i = 2尚未准备就绪,但i = 3线程已经开始;
  • t = 16,第五次std::async调用,i = 4启动一个新线程,然后移动。task(i = 3(的当前共享状态已经准备就绪,因此非阻塞;
  • t = 16 + e ,在循环之外,调用.get()等待 *共享状态' 准备就绪;
  • t = 18共享状态变得准备就绪,所以整个东西结束了。

std::future::operator=的标准细节:

以下是operator= std::future的标准报价:

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

影响:

  • (10.1( — 释放任何共享状态 (30.6.4(。

以下是"释放任何共享状态">的含义(重点是我的(:

当异步返回对象或异步提供程序被称为释放其共享状态时,这意味着:

(5.1( — [...]

(5.2( — [...]

(5.3( — 这些操作不会阻止共享状态变为就绪状态,但如果满足以下所有条件,则可能会阻止:共享状态是通过调用 std::async 创建的,共享状态尚未 就绪,这是对共享状态的最后一次引用

你的案子属于我强调的(我认为(。您使用 std::async 创建了共享状态,它处于睡眠状态(因此尚未准备好(,并且您只有一个对它的引用,因此这可能是阻塞。

保证在打印结果时执行所有任务,

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

任务

是否会一个接一个地执行(即任务分配会阻塞(。

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

[futures.unique_future]

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

  1. 影响:

    释放任何共享状态([futures.state](。

[期货状态]

  1. 当异步返回对象或异步提供程序被称为释放其共享状态时,这意味着:

    • 如果返回对象或提供程序保存对其共享状态的最后一个引用,则共享状态将被销毁;和

    • 返回对象或提供程序放弃对其共享状态的引用;以及

    • 这些操作不会阻止共享状态变为就绪状态,但如果满足以下所有条件,则可能会阻止:共享状态是通过调用 std::async 创建的,共享状态尚未就绪,这是对共享状态的最后一次引用。

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