Effective modern C++ 条款 38:如果异步至关重要请指定std::launch::async

    当你调用std::async来执行一个函数(或其他可执行体),你通常是想要异步执行这个函数的。但是std::async并不一定会那样做。实际上你是在要求函数按照std::async默认启动策略来运行函数。有两个标准策略,它们分别代表std::launch中的一个scoped枚举(详见Item10)。假设一个函数 f 被传至std::async来执行,
  • std::launch::async运行策略表示函数 f 必须异步执行,即在另一个线程上执行。
  • std::launch::deferred运行策略表示函数 f 只有当std::async返回的future对象调用get或者wait时,才会执行。也就是说,函数 f 推迟到std::future对象调用get或wait时才执行。当get或者wairt在缓刑线程中被调用时,f才会异步执行,(调用者会阻塞知道 f 执行完毕)。如果get和wait都没有被调用,则f永远不会执行。
    也许让人感到惊讶的是,std::async的默认运行策略--当你没有指定时采用的策略--并不是上述二者,而是二者的结合。下述两个调用完全相同:
     
    auto fut1 = std::async(f); // run f using default launch policy
    auto fut2 = std::async(std::launch::async | // run f either
                           std::launch::deferred, // async or
                           f); // deferred

    默认策略允许f异步运行,也允许其同步运行。正如Item37指出,这种策略允许std::async以及标准库的线程管理模块全权负责线程的创建、销毁、避免超额申请、以及负载均衡。正式这些菜使得std::async使用起来如此方便。
    但是std::async的默认策略还有一些值得一提的影响。假设一个线程 t 中执行以下语句,     
auto fut = std::async(f); // run f using default launch policy
  • 我们无法知道 f 是否会与 t 并行执行,因为 f 有可能会被推迟执行
  • 我们无法知道 f 是否会在另外一个线程中执行。
  • 我们甚至无法预知 f 是否会执行,因为无法保证在所有路径上,std::async返回的future对象的get或者wait方法都会被调用
    默认执行策略几乎不能与thread_local变量混用,因为混用意味着 f 要读或写这些线程本地存储(thread-local storage - TLS),但我们并不知道究竟会访问哪一个线程的变量:    
auto fut = std::async(f); // TLS for f possibly for different thread, but possibly for current thread
     同样基于wait调用利用超时机制的循环也比较复杂,因为在一个被采用std::launch::deferred策略的任务上调用wait_for或者wait_until。这意味着一下的循环看上去最终会结束,然实际上有可能会无限循环下去:    
void f() // f sleeps for
    { // 1 second,
        std::this_thread::sleep_for( // then returns
        std::chrono::seconds(1)
        );
    }
    auto fut = std::async(f);
    while (fut.wait_for( //loop until f finished 
                     std::chrono::microseconds(100) //while this may run forever
                     ) != std::future_status::ready)
     {... } 
    如果 f 在另外一个线程里并发执行(std::async选择std::launch::async策略),这段代码没有问题,但是如果选择的是std::launch::defered策略,则fut.wait_for调用会永远返回std::future_status::defered,永远也不会返回std::future_status::ready,所以循环会永远无法终止。
    这种bug在开发和单元测试阶段很容易被忽视,因为这种bug只有在系统负载过重是才会体现。负载过重才会导致线程超额申请、线程资源耗尽,这种情况下一个任务才很有可能被推迟执行。毕竟如果硬件并不存在线程超额申请或资源耗尽的情况时,系统没有理由不采用并行的方式来运行任务。
    要修正这种问题很简单:只需要通过std::async返回的future对象检查任务是否为推迟了,如果是那就避免上述的基于超时机制的循环。然而不幸的是,并没有直接的方法来通过future对象来检查其任务是否被推迟了。你不得不调用一个基于超时的函数--一个类似于wait_for的函数。当然,你并不真想想要等待什么,你只是想看它的返回值是否是std::future_status::deferred,因此我们只需要等待0秒超时即可:      
 auto fut = std::async(f); 
    if (fut.wait_for(std::chrono::seconds(0)) != std::future_status::deferred) 
     {
          while (fut.wait_for( //loop until f finished
               std::chrono::microseconds(100) //while this may run forever
               ) != std::future_status::ready)
             {...} 
           ...
     }
     else {...} 
    C++14在检查任务是否为延迟方面并没有任何的提升,但它却使指定时间区间(time duration)上更加方便,因为它利用了C++ 11对用户定义文本(user-defined literals)的特性来为时间加后缀,秒(s)、毫秒(ms)、小时(h)等。这些后缀在std::literals命名空间中定义,所以上述的代码可以改写为:    
using namespace std::literals; // for duration suffixes
    if (fut.wait_for(0s) != std::future_status::deferred) { // C++14
         while (fut.wait_for(100ms) != // only
                std::future_status::ready) {
          …
        }
     …
    只要满足以下条件,就可以采用默认启动策略调用std::async来执行任务:
  • 任务的执行不要求与调用线程并行执行
  • 不关心访问哪个线程的thread_local变量
  • 要么能够保证返回的future对象的get或者wait方法肯定会调用,或者允许任务不被执行
  • 保证使用wait_for或者wait_until的代码检查任务是否为推迟状态
    如果任何一个条件不能满足,你应该考虑确保std::async异步执行任务,你只需将std::launch::async作为std::async的第一个参数即可:
    auto fut = std::async(std::launch::async, f);
    事实上,如果能有一个函数功能与std::async一样,但是自动采用std::launch::async作为启动策略,会是一个很方便的工具,而这么做也很简单:  
template<typename F, class... Args>
     inline
      std::future<typename std::result_of<F(Args...)>::type>
      reallyAsync(F&& f, Args&&... args)
     {
      return std::async(std::launch::async,
       std::forward<F>(f),
       std::forward<Args>(args)...);
     }
    这个函数接受一个执行体 f 以及0个或多个参数args,并将std::forward它们的返回值传给std::async,采用std::launch::async启动策略。与std::async一样,它也返回std::future对象。要决定执行体 f 的返回值类型很简单,因为类型属性(type traits)std::result_of会告诉你(详见Item 9)。
    除了它可能会产生异常之外(表示无法创建新的线程,详见Item 37),reallyAsync函数使用与std::async一致。          
auto fut = reallyAsync(f); // run f asynchronously or throw std::system_error 
    在C++ 14可以推到reallyAsync的返回值类型,所以它的定义可以如下:  
template<typename F, class... Args>
     inline
      auto
      reallyAsync(F&& f, Args&&... args)
     {
      return std::async(std::launch::async,
       std::forward<F>(f),
       std::forward<Args>(args)...);
     }
    这个版本使得reallyAsync更为清晰的表明,它只是采用std::launch::async启动策略来启动std::async。
重点回顾
  • std::async的默认启动策略既是同步也是异步执行任务
  • 默认启动策略的自由性导致访问thread_local变量的不确定性、任务可能永远不会执行,以及基于超时的wait调用
  • 如果异步执行至关重要,则采用std::launch::async启动策略
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值