条款36:如果异步是必要的,则指定std::launch::async

当调用std::async来执行一个函数(或可调用对象)时,你基本是上都会想让函数以异步方式运行。但仅仅通过std::async来运行,你实际上要求的并非一定会达成异步运行的结果,你要求的仅仅是让该函数以符合std::async的启动策略来运行。标准策略有二,它们都是用限定作用域的枚举型别std::launch中的枚举量(关于限定作用域的枚举,参见条款10)来表示的。假设函数f要传递给std::async以执行,则:

  • std::launch::async启动策略意味着函数f必须以异步方式运行。亦即,在另一线程之上执行。
  • std::launch::deferred启动策略意味函数f只会在std::async所返回的期值get或wait得到调用时才运行。亦即,执行会推迟至其中一个调用发生的时刻。当调用get或wait时,f会同步运行。即,调用方会阻塞至f运行结束为止。如果get或wait都没有得到调用,f是不会运行的。

也许有点出人意料,std::aysnc的默认启动策略,也就是你如果不积极指定一个的话,它采用的并非以上两者中的一者。相反地,它采用的是对二者进行或运算的结果。下面的两个调用有着完全相同的意义:

auto fut1 = std::async(f);    //采用默认启动
auto fut2 = std::async(std::launch::async |      //采用或者异步
                       std::launch::deferred,    //或者推迟的方式
                       f);                       //运行f

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

但以默认启动策略来使用std::async,会触及一些意味深长的暗流。给定线程t执行一语句,

auto fut = std::async(f);  //采用默认启动策略运行f

则:

  • 无法预知f是否会和t并发运行,因为f可能会被调度为推迟运行
  • 无法预知f是否运行在与调用fut和get或wait函数的线程不同的某线程之上。如果那个线程是t,那就是说无法预知f是否会运行在于t不同的某线程之上。
  • 连f是否会运行这件起码的事情都是无法预知的,这是因为无法保证在程序的每条路径上,fut的get和wait都会得到调用。

默认启动策略在调度上的弹性常会在使用thread_local变量时导致不明不白的混淆,因为这意味着如果f读或写此线程级局部存储时,无法预知会取到的是哪个线程的局部存储:

auto fut = std::async(f);   //f的TLS可能是和一个独立线程相关,
                            //但是也可能是和调用fut的get或wait的线程相关

它也会影响那些基于wati的循环中以超时为条件者,因为对任务(参见条款35)调用wait_for或者wait_until会产生std::launch::deferred一值。这意味着以下循环虽然貌似会最终停止,但是,实际上,可能会永远运行下去:

using namespace std::literals;  //关于C++14持续时长后缀,参见条款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能正常工作需要满足以下所有条件:

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

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

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

其实,如果有个函数能像std::async那样运作,只是它会自动使用std::launch::aysnc作为启动策略,那将是个趁手的工具,而且很不错的撰写这个函数一点不难,这是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)...);
}

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

realltAsync的用法就像std::async:

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

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

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

这一版清清楚楚地显示,reallyAsync所做的一起,就是以std::launch::asyn启动策略来调整了std::async。

要点速记

  • std::async的默认启动策略既允许任务以异步方式执行,也允许任务以同步方式执行。
  • 如此的弹性会导致使用thread_local变量时的不确定性,隐含着任务可能永远不会执行,还会影响运用了基于超时的wait调用的程序逻辑
  • 如果异步是必要的,则指定std::launch::async
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值