当调用std::async时,是可以指定启动策略的:
- std::launch::async 表示函数f必须以异步方式运行,也就是运行在其他线程上;
- std::launch::deferred 表示函数f会被推迟执行,直到调用get或wait时,才会被执行,且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
上述两种写法是等价的(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_for或wait_until的调用产生BUG。因为对于被延迟执行的任务,该任务没有被执行过时,调用wait_for或wait_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启动策略;