Item 36: Specify std::launch::async if asynchronicity is essential.

当调用std::async时,是可以指定启动策略的:

  • std::launch::async 表示函数f必须以异步方式运行,也就是运行在其他线程上;
  • std::launch::deferred 表示函数f会被推迟执行,直到调用getwait时,才会被执行,且f是被同步执行的。即调用方会阻塞至f运行结束为止。如果getwait都没有被调用,则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

上述两种写法是等价的(STL源码中就是这么写的)。那么,默认的策略就是允许 f 既可以以同步的方式执行,也可以以异步的方式执行。之所以有这么大的弹性,就是为了使std::async和STL的线程管理组件能够承担起线程的创建、销毁和避免超额订阅以及负载均衡的责任。

但是,std::async的默认策略也会有如下问题:

  • 无法预测f是否会与当前线程并发执行。因为 f 可能会被调度为延迟执行;
  • 无法预测 f 是否运行在 get 或 wait 调用时的线程上;
  • 甚至无法预测 f 是否已经执行了。因为无法保证一定会调用 get 或 wait;

默认策略还会导致线程局部变量的访问问题。因为无法预测会访问到哪个线程的线程局部变量:

auto fut = std::async(f); // TLS for f possibly for
                          // independent thread, but
                          // possibly for thread
                          // invoking get or wait on fut

最后,默认策略还会使wait_forwait_until的调用产生BUG。因为对于被延迟执行的任务,该任务没有被执行过时,调用wait_forwait_until时,会一直返回std::launch::deferred。那么,下面的代码就可能会一直在死循环:

using namespace std::literals;      // for C++14 duration suffixes; see Item 34

void f()                            // f sleeps for 1 second,  then returns
{
  std::this_thread::sleep_for(1s);
}

auto fut = std::async(f);             // run f asynchronously                                                   // (conceptually)

while (fut.wait_for(100ms) !=         // loop until f has
       std::future_status::ready)     // finished running...
{                                     // which may never happen!}

这种缺陷在开发和测试中很容易被忽略,因为它一般发生在负载失衡时,负载失衡会把计算机逼向超额订阅或者线程耗尽的境地,此时任务就可能会被延迟执行。如果计算机没有这些情况,运行期系统并不会推迟执行。

解决办法也很简单,通过给wait_for传递一个0,来判断任务是异步执行,还是同步执行,然后再做相应的逻辑业务:

auto fut = std::async(f);          // as above

if (fut.wait_for(0s) ==            // if task is
    std::future_status::deferred)  // deferred...
{
             // ...use wait or get on fut// to call f synchronously
  
} else {     // task isn't deferred
  while (fut.wait_for(100ms) !=          // infinite loop not
         std::future_status::ready) {    // possible (assuming
                                         // f finishes)// task is neither deferred nor ready,
                     // so do concurrent work until it's ready
  }// fut is ready
}

综上,建议满足下面所有条件,再使用默认的启动策略:

  • 当调用 get 或 wait 时,任务不需要并发执行;
  • 读/写哪个线程的thread_local变量并不重要;
  • 可以保证 get 或 wait 一定会被调用,或者任务不被执行也能接受;
  • 使用 wait_for 或 wait_until 时,需要考虑 std::launch::deferred 策略(如上面的传0s);

如果上面的任一条件满足不了,就还是乖乖的以异步方式启动吧,代码调用如下:

auto fut = std::async(std::launch::async, f);  // launch f asynchronously

这里给出一个自动选择std::launch::async启动策略的工具,C++ 11版本代码如下:

template<typename F, typename... Ts>  // C++11
inline
std::future<typename std::result_of<F(Ts...)>::type>
reallyAsync(F&& f, Ts&&... params)       // return future
{                                        // for asynchronous
  return std::async(std::launch::async,  // call to f(params...)
                    std::forward<F>(f),
                    std::forward<Ts>(params)...);
}

reallyAsync 接受一个可调用对象 f 和 多个参数 params,并完美转发给std::async ,同时使用 std::launch::async 策略。C++14 版本代码如下:

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)...);
}

Things to Remember

  • 默认启动策略允许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、付费专栏及课程。

余额充值