当调用std::async来执行一个函数(或可调用对象)时,你基本是上都会想让函数以异步方式运行。但仅仅通过std::async来运行,你实际上要求的并非一定会达成异步运行的结果,你要求的仅仅是让该函数以符合std::async的启动策略来运行。标准策略有二,它们都是用限定作用域的枚举型别std::launch中的枚举量(关于限定作用域的枚举,参见条款10)来表示的。假设函数f要传递给std::async以执行,则:
- std::launch::async启动策略意味着函数f必须以异步方式运行。亦即,在另一线程之上执行。
- std::launch::deferred启动策略意味函数f只会在std::async所返回的期值get或wait得到调用时才运行。亦即,执行会推迟至其中一个调用发生的时刻。当调用get或wait时,f会同步运行。即,调用方会阻塞至f运行结束为止。如果get或wait都没有得到调用,f是不会运行的。
也许有点出人意料,std::aysnc的默认启动策略,也就是你如果不积极指定一个的话,它采用的并非以上两者中的一者。相反地,它采用的是对二者进行或运算的结果。下面的两个调用有着完全相同的意义:
auto fut1 = std::async(f); //采用默认启动
auto fut2 = std::async(std::launch::async | //采用或者异步
std::launch::deferred, //或者推迟的方式
f); //运行f
这么一来,默认启动策略就允许f以异步或同步的方式运行皆可,正如条款35所指出的那样,这种弹性使得std::async与标准库的线程管理组件能够承担得起线程的创建和销毁、避免超订,以及负载均衡的责任。这也是使用std::async来做并发程序设计如此方便的诸多因素中的一部分。
但以默认启动策略来使用std::async,会触及一些意味深长的暗流。给定线程t执行一语句,
auto fut = std::async(f); //采用默认启动策略运行f
则:
- 无法预知f是否会和t并发运行,因为f可能会被调度为推迟运行
- 无法预知f是否运行在与调用fut和get或wait函数的线程不同的某线程之上。如果那个线程是t,那就是说无法预知f是否会运行在于t不同的某线程之上。
- 连f是否会运行这件起码的事情都是无法预知的,这是因为无法保证在程序的每条路径上,fut的get和wait都会得到调用。
默认启动策略在调度上的弹性常会在使用thread_local变量时导致不明不白的混淆,因为这意味着如果f读或写此线程级局部存储时,无法预知会取到的是哪个线程的局部存储:
auto fut = std::async(f); //f的TLS可能是和一个独立线程相关,
//但是也可能是和调用fut的get或wait的线程相关
它也会影响那些基于wati的循环中以超时为条件者,因为对任务(参见条款35)调用wait_for或者wait_until会产生std::launch::deferred一值。这意味着以下循环虽然貌似会最终停止,但是,实际上,可能会永远运行下去:
using namespace std::literals; //关于C++14持续时长后缀,参见条款34
void f() //f睡眠1秒后返回
{
std::this_thread::sleep_for(1s);
}
auto fut = std::async(f); //以异步方式运行f(说说而已)
while(fut.wait_for(100ms) != //循环至
std::future_status::ready) //f完成运行...
{ //但此事可能永远不会发生!
...
}
如果f与调用std::async的线程是并发执行的(即选用了std::launch::async启动策略),这就没有问题(假定f最终会完成执行),但如果f被推迟执行,则fut.wait_for将总返回std::future_status::deferred。而那永远也不会取值std::future_status::ready,所以循环也就永远不会终止。
这一类缺陷在开发或测试单元测试中很容易被忽略,因为它只会在运行负载很重时才会现身。这样的负载是把计算机逼向超订或线程耗尽的条件,而那就是任务很可能会被推迟的机会了。毕竟,如果硬件层面没有面临超订或者线程耗尽的威胁,运行期系统并无理由不去调度任务以并发方式执行。
修正这个缺陷并不难:校验std::async返回的期值,确定任务是否被推迟,然后如果确定被推迟了,则避免进入基于超时的循环。不幸的是,没有直接的办法来询问期值任务是否被推迟了。作为替代手法,必须先调用一个基于超时的函数,例如wait_for.在此情况下,你其实并不是要等待任何事情,而只是要查看返回值是否为std::future_status::deferred,所以请搁置你的怀疑,径自调用一个超时为零的wait_for即可:
auto fut = std::async(f); //同上
if (fut.wait_for(0s) == //如果任务
std::future_status::deferred) //被推迟了....
{
... //....则使用fut的wait或get以异步方式调用f
}
else //任务未被推迟
{
while(fut.wait_for(100ms) != //不可能死循环
std::future_status::ready) //(前提假定f会结束)
{
... //任务既未被推迟,也未就绪
//则做并发工作,直至任务就绪
}
... //fut就绪
}
将上述诸种因素纳入考量的总体结论是:以默认启动策略对任务使用std::async能正常工作需要满足以下所有条件:
- 任务不需要与调用get或wait的线程并发执行。
- 读/写哪个线程的thread_local变量并无影响
- 或者可以给出保证在std::async返回的期值上调用get或wait,或者可以接受任务可能永不执行
- 使用wait_for或wait_unitil的代码会将任务被推迟的可能性纳入考量
只要其中一个条件不满足,你就很有可能想要确保任务以异步方式执行。实现这一点的手法,就是在调用时把std::launch::async作为第一个实参传递:
auto fut = std::async(std::launch::async, f); //以异步方式启动f
其实,如果有个函数能像std::async那样运作,只是它会自动使用std::launch::aysnc作为启动策略,那将是个趁手的工具,而且很不错的撰写这个函数一点不难,这是C++11版本:
template<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, //调用f(params...)
std::forward<F>(f),
std::forward<Ts>(params)...);
}
该函数接受一个可调用对象,以及零个或多个形参params,并将后者完美转发给std::async,同时传递std::launch::async作为启动策略。就像std::async,它会返回一个型别为std::future的对象作为使用params调用f的结果。决定该结果的型别很容易,std::result_of这个型别特征就会把结果给到你了(有关型别特性的一般材料,参见条款9)
realltAsync的用法就像std::async:
auto fut = reallyAync(f); //以异步方式运行f
//如果std::async会抛出异常
//reallyAsync也会抛出异常
在C++14中,对reallyAsync返回值进行推导型别的能力使函数声明得以简化:
template<typename F, typename ...Ts>
inline auto reallyAsync(F&& f, Ts&&... params)
{
return std::async(std::launch::async,
std::forward<F>(f),
std::forward<Ts>(paramss),
}
这一版清清楚楚地显示,reallyAsync所做的一起,就是以std::launch::asyn启动策略来调整了std::async。
要点速记
- std::async的默认启动策略既允许任务以异步方式执行,也允许任务以同步方式执行。
- 如此的弹性会导致使用thread_local变量时的不确定性,隐含着任务可能永远不会执行,还会影响运用了基于超时的wait调用的程序逻辑
- 如果异步是必要的,则指定std::launch::async