异步是否总是在C++中使用另一个线程/内核/进程

Does async always use another thread/core/process in C++?

本文关键字:另一个 线程 内核 进程 是否 C++ 异步      更新时间:2023-10-16

据我所知,async在另一个线程/进程/内核中执行一个函数并且不会阻塞主线程,但情况总是如此吗?

我有以下代码:

async(launch::async,[]()
{
Sleep(1000);
puts("async");
});
puts("main");

它打印async main,那么这是否意味着主线程会等到async完成?

如果我更改为以下内容:

auto f = async(launch::async,[]() // add "auto f = "
{
Sleep(1000);
puts("async");
});
puts("main");

它打印main async.这使得 main 看起来不会等待async完成。

据我所知,异步在另一个线程/进程/核心中执行一个函数并且不会阻塞主线程,但它总是发生吗?

仅当std::launch::async作为第一个参数传递时,std::async才能保证在单独的线程上执行:

  • std::launch::async:启动一个新线程以异步执行任务
  • std::launch::deferred第一次请求任务结果时在调用线程上执行任务(延迟求值)

默认启动策略std::launch::async | std::launch::deferred


std::async返回std::future.std::future的析构函数只有在从std::async返回未来时才会阻塞:

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


  • 在你的第一个代码片段中,你创建一个立即被销毁右值表达式- 因此"async"将在"main"之前打印。

    1. 异步匿名函数被创建并开始执行。

    2. 异步匿名函数被销毁。

      • main执行被阻止,直到函数完成。

      • 打印"async"

    3. main执行恢复。

      • 打印"main"

  • 在第二个代码段中,创建一个左值表达式,其生存期绑定到变量ff将在main函数的作用域结束时销毁- 因此"main"由于Delay(1000),将在"async"之前打印。

    1. 异步匿名函数被创建并开始执行。

      • 有一种Delay(1000)会延迟"async"立即打印。
    2. main执行将继续。

      • 打印"main"
    3. main的范围结束。

    4. 异步匿名函数被销毁。

      • main执行将被阻止,直到函数完成。

      • 打印"async"

它打印async main,那么这是否意味着主线程会等到async完成?

是的,确实如此,但那是因为您没有从async捕获返回的未来。async的特殊之处在于,从它返回的future在析构函数中阻塞,直到线程完成。 由于您没有捕获返回的future

async(launch::async,[]()
{
Sleep(1000);
puts("async");
});

必须在当前线程中取得进展之前完成,因为返回的future在表达式末尾被销毁。

它打印main async.这使得 main 看起来不会等待async完成。

当你打电话给async时,这才是你真正想要的。 由于您已经捕获了未来,因此在异步任务完成时,您的主线程可以继续。 由于该线程有延迟,因此main将在线程之前打印。

如果传递std::launch::async,则std::async必须像在自己的线程中运行一样运行任务。

C++线程的唯一概念是std::thread

std::async返回具有唯一属性的std::future;如果销毁,它将阻止存储在std::async中的任务完成。 当您无法捕获返回值时,这会捕获您;返回的std::future是一个未命名的临时存在,并在"该行的尽头"被销毁。

此销毁等待async任务完成。

在您存储它的情况下,此延迟会等到变量f被销毁,即在main的末尾,也就是我们打印之后。

请注意,至少 C++11 的一个主要实现 MSVC 2015 和 2017 最多具有使用线程池而不是新线程的勉强兼容std::async。 此线程池意味着一组长时间运行的async调用可能会使其他async调用无法运行。

使用线程池是合法的(只要它重新创建任何线程局部),但如果所有现有线程都忙于"太长时间",它应该尽量避免饥饿并创建新线程。

它勉强兼容,因为该标准仅指出线程"应该"向前推进。 由于随机原因而从不进展的线程在C++下是合法的;从某种意义上说,你可以争辩说,这就是std::async在这些情况下效仿的,从而通过了假设测试。

这是因为std::future析构函数(从std::async返回)等待其任务完成。

在第一个代码片段中,从std::async返回的临时std::future对象在语句末尾被销毁,因为如 https://en.cppreference.com/w/cpp/language/lifetime

所有临时对象都将被销毁,作为评估 (在词法上)包含它们所在的点的完整表达式 创建

因此,在执行下一条语句之前,std::future对象的析构函数会阻止直到任务完成,这意味着puts("async")puts("main")之前执行。

但是,在第二个代码片段中,std::async 的返回值被移动到本地对象中,该对象在退出作用域时被销毁。因此,与async的线路在没有阻塞的情况下执行,并且puts("main")puts("async")之前执行(被Sleep调用阻塞)。

此行为在 https://en.cppreference.com/w/cpp/thread/async 中解释为:

如果从 std::async 获得的 std::future 未从 std::async 移出或绑定 对于引用,std::future 的析构函数将在 完整表达式的结束,直到异步操作完成, 本质上是使代码同步如下:

std::async(std::launch::async, []{ f(); }); // temporary's dtor waits for f()
std::async(std::launch::async, []{ g(); }); // does not start until f() completes

在《有效的现代C++》一书的第38项中,这表示为:

最后一个未来的析构函数引用 通过 std::async 块启动的非延迟任务,直到任务 完成。从本质上讲,这种未来的析构函数确实 异步执行任务的线程上的隐式联接 正在运行。