当你调用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永远不会执行。
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
但是std::async的默认策略还有一些值得一提的影响。假设一个线程 t 中执行以下语句,
auto fut = std::async(f); // run f using default launch policy
- 我们无法知道 f 是否会与 t 并行执行,因为 f 有可能会被推迟执行
- 我们无法知道 f 是否会在另外一个线程中执行。
- 我们甚至无法预知 f 是否会执行,因为无法保证在所有路径上,std::async返回的future对象的get或者wait方法都会被调用
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的代码检查任务是否为推迟状态
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启动策略