【问题标题】:Does async always use another thread/core/process in C++?异步是否总是在 C++ 中使用另一个线程/核心/进程?
【发布时间】:2017-08-14 13:20:13
【问题描述】:

据我所知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 完成。

【问题讨论】:

    标签: c++ multithreading asynchronous concurrency


    【解决方案1】:

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

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

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

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


    std::async 返回std::futurestd::future's destructor 仅当从 std::async 返回未来时才会阻塞:

    这些操作不会阻塞共享状态准备就绪,除非满足以下所有条件它可能会阻塞:共享状态是通过调用 std::async 创建的,共享状态尚未准备好,这是对共享状态的最后一次引用


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

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

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

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

        • "async" 已打印。

      3. main 继续执行。

        • "main" 已打印。

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

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

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

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

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

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

        • "async" 已打印。

    【讨论】:

    • deferred 似乎并不真正有用,除非您不知道以后是否需要结果。
    • @JAB:惰性求值被认为足够有用,以至于某些语言默认是惰性求值的;)
    【解决方案2】:

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

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

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

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

    它打印main async。这看起来好像 main 不等待 async 完成。

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

    【讨论】:

      【解决方案3】:

      如果您传递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 在这些情况下所模仿的,从而通过了 as-if 测试。

      【讨论】:

        【解决方案4】:

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

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

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

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

        然而,在第二个代码 sn-p 中,std::async 的返回值被移动到一个本地对象中,当退出范围时该对象被销毁。因此,async 的行在没有阻塞的情况下执行,puts("main")puts("async")(被Sleep 调用阻塞)之前执行,正如预期的那样。

        https://en.cppreference.com/w/cpp/thread/async 中将这种行为解释为:

        如果从 std::async 获得的 std::future 没有被移出或绑定 对于引用,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
        

        Effective Modern C++这本书的Item 38中,这表示为:

        最后一个future的析构函数引用一个共享状态 通过 std::async 块启动的非延迟任务,直到任务 完成。本质上,这样一个未来的析构函数做了一个 异步执行任务所在线程的隐式连接 正在运行。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2010-10-10
          • 2022-01-08
          • 2022-01-08
          • 2020-10-23
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多