前言
当开发者决定异步执行某函数或其他可调用对象时,往往会选择调用std::async,但实际上这种做法并没有要求异步执行,当你明确需要使用异步时,你应当使用std::launch::async。
启动策略
std::lauch是一个enum class,其中含有两个枚举量,分别代表着不同的异步启动策略。假定当前存在一个函数f被传递至std::async
- std::launch::async launch policy意味着函数f必然将运行于另一个线程之上(即异步运行)
- std::launch::deferred launch policy意味着f只有在std::async返回的future对象上调用get或wait时才会运行
换而言之,f的执行被推迟至调用get或wait时。当调用get或wait时,f将同步执行(即调用程序将阻塞,直到f完成运行)。如果一直不调用get或wait,f将永不执行。
默认启动策略
更加让人惊讶地是,std::async的默认启动策略并非是二者中的某一种,而是二者的综合,也就是说下述两种调用其实时等价的:1
2auto fut1 = std::async(f); // run f using default launch policy
auto fut2 = std::async(std::launch::async|std::launch::deferred,f);
因此,默认启动策略允许f以异步或同步方式运行。如Item35所述,这种灵活性允许std::async与标准库中线程管理组件承担线程的创建和销毁任务,避免触发oversubscription以及保证负载均衡,因此使用std::async可以保证开发者能够高效完成并发编程。
默认启动策略特性
std::async的默认启动策略还有一些值得一提的因素。假设当前存在一个线程t正在执行以下语句:1
auto fut = std::async(f); // run f using default launch policy
我们将得出以下结论:
- 无法预测f是否将与t同时运行,因为f可能被安排为推迟执行(因为不明确调用get或wait的时机)。
- 无法预测f是否会在一个不同于调用fut之get或wait的线程上执行,本例即无法预测f的执行线程是否不同于t。
- 无法预测f是否会运行,因为可能无法保证在程序的所有路径上都会调用fut的get或wait。
默认启动策略几乎不能与thread_local变量混用,因为混用意味着f要读或写thread-local storage(TLS) ,但我们并不知道究竟会访问哪一个线程中的变量:1
2// TLS for f possibly for independent thread, but possibly for thread invoking get or wait on fut
auto fut = std::async(f);
默认启动策略还会影响使用timeout的wait-based loop,因为在被推迟的task(见Item35)上调用wait_for或wait_until会产生std::launch::deferred。这意味着以下循环看起来似乎会终止,实际上也许将永远运行:1
2
3
4
5
6
7
8using 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) != std::future_status::ready){ // loop until f has finished running...
…// may be always runing
}
如果f与调用std::async的线程同时运行(即启动策略为std::launch::async),那么该程序毫无问题。但如果f推迟运行,那么fut.wait_for将永远返回std::future_status::deferred,因而造成死循环。
这种bug在开发和单元测试阶段很容易被忽视,因为它只有在系统负载过重时才会出现。要修正该问题也很简单:只需通过std::async返回的future对象检查task是否被推迟,如果是则并不执行循环。然而不幸的是,并没有直接方法来检查tsk是否被推迟,因此我们不得不调用一个基于time-out的函数(比如wait_for)。因为我们只是需要明确task状态,而不是真的想等待什么,因此等待参数设为0s即可:1
2
3
4
5
6
7
8
9auto fut = std::async(f);
if (fut.wait_for(0s) == 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) != std::future_status::ready) {
… // task is neither deferred nor ready,so do concurrent work until it's ready
}
… // fut is ready
}
默认启动策略使用条件
只要满足以下条件,我们就可以放心大胆的采用默认启动策略执行task:
- task无需与调用wait或get的线程并行执行
- 无需关注存取哪个线程的thread_local变量
- 要么能够保证返回future对象的get或者wait方法一定会被调用,或者允许task不被执行
- 在使用wait_for和wait_unitl时检测task状态是否为defered
只要上述条件有一个不满足,那我们都应当使用std::lunch::async确保异步执行task:1
auto fut = std::async(std::launch::async, f); // launch f asynchronously
事实上,如果能有一个函数功能与std::async一样,但是自动采用std::launch::async作为启动策略,会是一个很方便的工具,但实现它也不难,以下为C++11版本:1
2
3
4
5
6template<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,std::forward<F>(f),std::forward<Ts>(params)...);
}
C++14由于返回值类型推衍的原因,实现可以得到进一步的简化:1
2
3
4
5template<typename F, typename... Ts>
inline
auto reallyAsync(F&& f, Ts&&... params){
return std::async(std::launch::async,std::forward<F>(f),std::forward<Ts>(params)...);
}
总结
- std::async的默认启动策略既同步也异步
- 默认启动策略可能会导致TLS访问的不确定性、以及wait_for、wait_until的使用时状态检查
- 如果异步执行至关重要,请采用std::launch::async启动策略