使⽤std::async代替std::thread启动异步任务
std::thread
c++11在语⾔层⾯对并发编程提供了有⼒的⽀持,std::thread就是⼀例,它以线程的⽅式启动异步任务。
关于thread创建线程对象并使⽤的⽤法,请参考
使⽤thread对象,并在其上运⾏⼀个函数,这是基于线程的程序设计:
int DoAsyncWork();
std::thread t(DoAsyncWork);
std::async
还有⼀种更好的⽅式:基于任务的程序设计。即把任务函数传递给std::async,⽰例:
#include<future>
auto fut = std::async(DoAsyncWork);// fut是期值
thread技术
之所以说基于任务的程序设计更好,理由如下:
基于任务的⽅法可以获取函数DoAsyncWork的返回值,⽽基于线程的则困难的多
基于任务的程序表现着更⾼阶的抽象,应⽤程序不必进⾏线程细节的管理
线程细节的管理,包括但不限于:
当使⽤thread创建线程时,如果试图创建的线程数量多于系统能够提供的数据时,会抛出std::system_error异常
即使没有⽤尽线程,也要注意超订问题(就绪状态的软件线程数量超过了硬件线程,线程调度器会为软件线程在硬件线程上分配CPU时间⽚,这会导致语境切换,进⽽导致线程开销增⼤)
⽽std::async则把所有问题扔给了C++标准库的实现者。除⾮你认为你可以⽐标准库做的更好,否则就不要尝试⼿动处理以上问题了。
关于std::async,需要知道它的两种启动策略:
std::launch::async: 函数f必须以异步⽅式运⾏,即在另⼀线程上执⾏
std::launch::deferred: 函数f只会在std:async所返回的期值的get或wait得到调⽤时才执⾏,即推迟执⾏到其中⼀个调⽤发⽣的时刻在调⽤get/wait时,f会同步运⾏
调⽤⽅会阻塞到f运⾏结束
如果get/wait都没有得到调⽤,f不会运⾏
⽽std::async的默认启动策略,并不是以上两个中的⼀个,⽽是对⼆者进⾏或运⾏的结果。下⾯两个调⽤意义相同:
auto fut1 = std::async(f);// 默认⽅式
auto fut2 = std::async(std::launch::async | std::launch::deferred, f);// 采⽤或者异步或者推迟的⽅式
这么⼀来,默认策略就允许f以异步或者同步的⽅式运⾏皆可。这种弹性使得标准库的线程管理组件能够承担得起线程创建、销毁、避免超订和负载均衡的任务。
std::async的注意事项
如前所述,以默认策略启动std::async时,会有以下问题:
⽆法知道f是否会和t并发运⾏,因为f可能会被调度为推迟运⾏
⽆法预知f是否运⾏在与调⽤fut的get/wait函数的线程不同的某个线程之上
甚⾄⽆法预知f是否会得到运⾏,因为⽆法保证在程序的每条路径上,fut的get/wait都会得到调⽤
在使⽤thread_local变量时会导致混淆,如果f读写此线程局部存储时,⽆法预知会取到哪个线程的局部存储
可能导致基于wait的循环中以超时为条件者陷⼊死循环,因为对任务调⽤wait_for/wait_until会产⽣std::launch::deferred,如下:using namespace std::literals // c++14持续时长后缀
void f()
{
std::this_thread::sleep_for(1s);
}
auto fut = std::async(f);// 异步调⽤f,默认策略
while(fut.wait_for(100ms)!= std::future_status::ready){// 等待f完成,可能在此处死循环
// ...
}
上例中,如果f是并发执⾏的,不会产⽣问题。
但如果f被推迟执⾏,fut.wait_for将总是返回std::future_status::deferred,所以陷⼊死循环。
避免这种问题也不难,只需要校验std:async的返回值,如果被推迟,则不要进⼊基于超时的循环:
auto fut = std::async(f);
if(fut.wait_for(0s)== std::future_status::deferred){
// f推迟运⾏,则使⽤fut的wait/get以异步⽅式调⽤f
}else{
while(fut.wait_for(100ms)!= std::future_status::ready){// 不会进⼊死循环,前提是确保f会结束
/
/ f未被推迟,也未就绪。则做并发⼯作,直到任务就绪
}
}
综上所述,只有在以下条件都满⾜时,才能使⽤std::async的默认启动策略:
任务不需要与调⽤get/wait的线程并发执⾏
读写哪个线程的thread_local变量⽆影响
要么可以给出保证在std::sync返回的期值之上调⽤get/wait,要么可以接受任务可能永不执⾏
使⽤wait_for/wait_until的代码将任务被推迟的可能性纳⼊考虑
其中任意⼀条⽆法保证时,请确保任务以异步⽅式执⾏:
auto fut = std::async(std::launch::async, f);
写个模板封装⼀下:
template<typename F,typename.. Ts>
inline std::future<typename std::result_of<)>::type>
reallyAsync(F&& f, Ts&&.. params)
{
return std::async(std::launch::async, std::forward<f>(f), std::forward<Ts>(params)...);
}
auto fut =reallyAsync(f);// 以异步⽅式运⾏f
⼩结
对于异步任务,可以按以下建议使⽤:
优先选⽤std::async,它是对线程更⾼层次的抽象
优先使⽤std::async的默认策略,除⾮不满⾜上述使⽤条件,这会给予标准库更⼤的线程管理弹性
如果不能使⽤默认策略,则使⽤std::launch::async
如果std::async满⾜不了使⽤需求,则使⽤std::thread,如:
需要访问底层线程实现的API,如pthread库,设置线程优先级和亲和性。std::thread提供了native_handle成员函数
需要且能够为应⽤优化线程⽤法,如执⾏时的性能剖析情况已知,且作为唯⼀的主要进程部署在⼀种硬件特性固定的平台上需要实现超越c++并发API的线程技术,如在c++实现中未提供的线程池的平台上实现线程池
参考资料
《Effective Modern C++》
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论