《Effective Modern C++》学习笔记之条款三十六:如果异步是必要的,则指定std::launch::async

对于std::async有两个执行策略可供选择,每个都是通过std::launch局部枚举来表示:

  • std::launch::async:意味着函数f必须异步执行,即在另一线程执行。
  • std::launch::deferred:意味着函数f只会在std::async所返回的期待值的getwait得到调用时才运行。当调用getwait时,f会同步运行,即调用方会阻塞至f运行结束为止。如果getwait都没有得到调用,f是不会运行的。

当你调用std::async来执行一个函数(或一个可执行对象)时,你通常希望函数是异步执行的,但是如果你没有显示指定它的执行策略,则它将使用默认策略执行,std::async的默认策略采用的并非是以上两者中的一者而是对两者进行或运算的结果。例如,下面两个调用有着完全相同的意义:

auto fut1 = std::async(f);       // 使用默认发射策略执行f

auto fut2 = std::async(std::launch::async |     // 使用async或deferred执行f
                       std::launch::deferred
                       f);

所以,默认启动策略就允许f以异步或同步的方式运行皆可,这种弹性使得std::async与标准库的线程管理组件能够承担得起线程的创建和销毁、避免超订,以及负载均衡的责任。这也是使用std::async来做并发程序设计如此方便的诸多因素的一部分。

但以默认启动策略来使用std::async会触及一些未知的问题。例如,在线程t中执行std::async(f):

  1. 无法预知f是否会和t并发运行,因为f可能会被调度为推迟到getwait得到调用时才运行。
  2. 无法预知f是否运行在与调用fut的get或wait函数的线程不同的某线程之上。
  3. 连f是否运行这件起码的事情都是无法预知的,这是因为无法保证在程序的每条路径上,fut的get或wait都会得到调用。
  4. 默认启动策略在调度上的弹性常会在使用thread_local变量时导致不明不白的混淆,因为这意味着如果f读或写此线程级局部存储(thread-local storage,TLS)时,无法预知会取到的是哪个线程的局部存储

std::async的默认启动策略也会是thread_local的使用变得不明不白,因为这意味着如果fdu或写此线程级局部存储(thread-local storage,TLS)时,无法预知会取到的是哪个线程的局部存储。

std::async的默认启动策略也会影响那些基于wait的循环中以超时为条件者,因为如果任务是以std::launch::deferred策略执行,则对其调用wait_for或者wait_until只会返回std::launch::deferred,代码如下:

using namespace std::literals;  //关于C++14持续时长后缀,参见Item 34

void f()                        //f睡眠1秒后返回
{
    std::this_thread::sleep_for(1s);
}

auto fut = std::async(f);       //以异步方式运行f(说说而已)

while(fut.wait_for(100ms) != std::future_status::ready) //循环至
{                                                       //f完成运行....
    .....                                               //但此事可能永远不会发生!
}

如果f与调用std::async的线程是并发执行的(即选用了std::launch::async启动策略),这就没问题(假定f最终会完成执行)。但如果f被推迟执行,则fut.wait_for将总会返回std::future_status::deferred。而那永远也不会取值std::future_status::ready,所以循环也就永远不会终止。

但是,这一类缺陷在开发或者单元测试中很容易被忽略,因为它只会在运行负载很重时,才会现身。这样的负载是把计算机逼向超订或线程耗尽的条件,而那就是任务很可能会被推迟的时机了。毕竟,如果硬件层面没有面临超订或者线程耗尽的威胁,运行期系统并无理由不去调度任务以并发方式执行。

而修正这个问题的方式并不难:校验std::async返回的期望值,确定任务是否被推迟,然后如果确实被推迟了,则避免进入基于超时的循环

不幸的是,没有直接的办法来询问期望值任务是否被推迟了。作为替代手法,必须先调用一个基于超时的函数,例如wait_for。在此情况下,你其实并不是要等待任何事情,而只是要查看返回值是否为std::future_status::deferred,所以请搁置你的怀疑,径直调用一个超时为零的wait_for即可:

auto fut = std::async(f);           //同上

if (fut.wait_for(0s) ==             //如果任务
    std::future_status::deferred)   //被推迟..
{
    
    ...             //则使用fut的wait或get以异步方式调用f

} else {            //任务未被推迟
    while (fut.wait_for(100ms) !=           //不可能死循环
            std::future_status::ready) {    //(前提假定f会结束)

                ...                     //任务既未被推迟,也未就绪,
                                        //则做并发工作,直至任务就绪
            }
    ....                            //fut 就绪
}   

将上述诸种因素纳入考量的总体结论是:以默认启动策略对任务使用std::async能将正常工作需要满足以下所有条件:

  1. 任务不需要与调用get或wait的线程并发执行。
  2. 读/写哪个线程的thread_local变量并无影响。
  3. 可以给出保证在std::async返回的期值之上调用get或wait,或者可以接受任务可能永不执行。
  4. 使用wait_for或wait_until的代码会将任务被推迟的可能性纳入考量。

只要其中一个条件不满足,你就很可能想要确保任务以异步方式执行。实现这一点的手法,就是在调用时把std::launch::async作为第一个实参传递:

auto fut = std::async(std::launch::async, f);       //以异步方式启动f

其实,如果有个函数能像std::async那样运作,只是它会自动使用std::launch::async作为启动策略,那将是个趁手的工具,而且很不错的是撰写这个函数一点不难。这是C++11版本:

template<typename F, typename ... Ts>
inline std::future<typename std::result_of<F(Ts...)>::type>
reallyAsync(F&& f, Ts&& ... params)             //返回异步所需的期值
{
    return std::async(std::launch::async,       //调用f(params...)
                        std::forward<F>(f),
                        std::forward<Ts>(params)...);
}

该函数接受一个可调用对象f,以及零个或多个形参params,并将后者完美转发(参见Item 25)给std::async,同时传递std::launch::async作为启动策略。就像std::async,它会返回一个型别为std::future的对象作为使用params调用f的结果。决定该结果的型别很容易,std::result_of这个型别特征就把结果给到你了(参见Item 9)。

reallyAsync的用法就像std::async:

auto fut = reallyAsync(f);          //以异步方式运行f
                                    //如果std::async会抛出异常
                                    //reallyAsync也会抛出异常

在C++14中,对reallyAsync返回值进行推导型别的能力使函数声明得以简化:

template<typename F, typename ... Ts>
inline auto                                     //C++14
reallyAsync(F&& f, Ts&& ... params)             
{
    return std::async(std::launch::async,       
                        std::forward<F>(f),
                        std::forward<Ts>(params)...);
}

这一版本清清楚楚的显示,reallyAsync所做的一切,就是以std::launch::async启动策略来调用了std::async

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Chiang木

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值