C++98
规范中并没有线程库的存在,直到C++11
中才终于提供了多线程的规范库,提供了治理线程、爱护共享数据、线程间同步操作、原子操作等类。多线程库对应的头文件是#include <thread>
,类名为std::thread
。
然而线程毕竟是比拟贴近零碎的货色,应用起来依然不是很不便,特地是线程同步及获取线程运行后果上就更加麻烦。咱们不能简略的通过thread.join()
失去后果,必须定义一个线程共享的变量来传递后果,同时还要思考线程间的互斥问题。好在C++11
中提供了一个绝对简略的异步接口std::async
,通过这个接口能够简略的创立线程并通过std::future
中获取后果。以往都是本人去封装线程实现本人的async,当初有线程的跨平台接口能够应用就极大的不便了C++多线程编程。
先看一下std::async
的函数原型
//(C++11 起) (C++17 前)
template< class Function, class... Args>
std::future<std::result_of_t<std::decay_t<Function>(std::decay_t<Args>...)>>
async( Function&& f, Args&&... args );
//(C++11 起) (C++17 前)
template< class Function, class... Args >
std::future<std::result_of_t<std::decay_t<Function>(std::decay_t<Args>...)>>
async( std::launch policy, Function&& f, Args&&... args );
第一个参数是线程的创立策略,有两种策略可供选择:
std::launch::async
:在调用async就开始创立线程。std::launch::deferred
:提早加载形式创立线程。调用async时不创立线程,直到调用了future的get或者wait时才创立线程。
默认策略是:std::launch::async | std::launch::deferred
也就是两种策略的合集,具体什么意思前面具体再说
第二个参数是线程函数
线程函数可承受function, lambda expression, bind expression, or another function object
第三个参数是线程函数的参数
不再阐明
返回值std::future
std::future
是一个模板类,它提供了一种拜访异步操作后果的机制。从字面意思上看它示意将来,这个意思就十分贴切,因为她不是立刻获取后果然而能够在某个时候以同步的形式来获取后果。咱们能够通过查问future的状态来获取异步操作的构造。future_status有三种状态:
- deferred:异步操作还未开始
- ready:异步操作曾经实现
- timeout:异步操作超时,次要用于std::future<T>.wait_for()
示例:
//查问future的状态
std::future_status status;
do {
status = future.wait_for(std::chrono::seconds(1));
if (status == std::future_status::deferred) {
std::cout << "deferred" << std::endl;
} else if (status == std::future_status::timeout) {
std::cout << "timeout" << std::endl;
} else if (status == std::future_status::ready) {
std::cout << "ready!" << std::endl;
}
} while (status != std::future_status::ready);
std::future
获取后果的形式有三种:
- get:期待异步操作完结并返回后果
- wait:期待异步操作完结,但没有返回值
- waite_for:超时期待返回后果,下面示例中就是对超时期待的应用展现
介绍完了std::async
的函数原型,那么它到底该如何应用呢?
std::async
的根本用法:示例链接
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
#include <future>
#include <string>
#include <mutex>
std::mutex m;
struct X {
void foo(int i, const std::string& str) {
std::lock_guard<std::mutex> lk(m);
std::cout << str << ' ' << i << '\n';
}
void bar(const std::string& str) {
std::lock_guard<std::mutex> lk(m);
std::cout << str << '\n';
}
int operator()(int i) {
std::lock_guard<std::mutex> lk(m);
std::cout << i << '\n';
return i + 10;
}};
template <typename RandomIt>int parallel_sum(RandomIt beg, RandomIt end){
auto len = end - beg;
if (len < 1000)
return std::accumulate(beg, end, 0);
RandomIt mid = beg + len/2;
auto handle = std::async(std::launch::async,
parallel_sum<RandomIt>, mid, end);
int sum = parallel_sum(beg, mid);
return sum + handle.get();
}
int main(){
std::vector<int> v(10000, 1);
std::cout << "The sum is " << parallel_sum(v.begin(), v.end()) << '\n';
X x;
// 以默认策略调用 x.foo(42, "Hello") :
// 可能同时打印 "Hello 42" 或提早执行
auto a1 = std::async(&X::foo, &x, 42, "Hello");
// 以 deferred 策略调用 x.bar("world!")
// 调用 a2.get() 或 a2.wait() 时打印 "world!"
auto a2 = std::async(std::launch::deferred, &X::bar, x, "world!");
// 以 async 策略调用 X()(43) :
// 同时打印 "43"
auto a3 = std::async(std::launch::async, X(), 43);
a2.wait(); // 打印 "world!"
std::cout << a3.get() << '\n'; // 打印 "53"
} // 若 a1 在此点未实现,则 a1 的析构函数在此打印 "Hello 42"
可能的后果
The sum is 10000
43
world!
53
Hello 42
由此可见,std::async
是异步操作做了一个很好的封装,使咱们不必关注线程创立外部细节,就能不便的获取异步执行状态和后果,还能够指定线程创立策略。
深刻了解线程创立策略
- std::launch::async调度策略意味着函数必须异步执行,即在另一线程执行。
- std::launch::deferred调度策略意味着函数可能只会在std::async返回的future对象调用get或wait时执行。那就是,执行会推延到其中一个调用产生。当调用get或wait时,函数会同步执行,即调用者会阻塞直到函数运行完结。如果get或wait没有被调用,函数就相对不会执行。
两者策略都很明确,然而该函数的默认策略却很乏味,它不是你显示指定的,也就是第一个函数原型中所用的策略即std::launch::async | std::launch::deferred
,c++规范中给出的阐明是:
进行异步执行还是惰性求值取决于实现
auto future = std::async(func); // 应用默认发射模式执行func
这种调度策略咱们没有方法预知函数func是否会在哪个线程执行,甚至无奈预知会不会被执行,因为func可能会被调度为推延执行,即调用get或wait的时候执行,而get或wait是否会被执行或者在哪个线程执行都无奈预知。
同时这种调度策略的灵活性还会混同应用thread_local变量,这意味着如果func写或读这种线程本地存储(Thread Local Storage,TLS),预知取到哪个线程的本地变量是不可能的。
它也影响了基于wait循环中的超时状况,因为调度策略可能为deferred
的,调用wait_for或者wait_until会返回值std::launch::deferred。这意味着上面的循环,看起来最终会进行,然而,实际上可能会始终运行:
void func() // f睡眠1秒后返回
{
std::this_thread::sleep_for(1);
}
auto future = std::async(func); // (概念上)异步执行f
while(fut.wait_for(100ms) != // 循环直到f执行完结
std::future_status::ready) // 但这可能永远不会产生
{
...
}
为防止陷入死循环,咱们必须查看future是否把工作推延,然而future无奈获知工作是否被推延,一个好的技巧就是通过wait_for(0)来获取future_status是否是deferred:
auto future = std::async(func); // (概念上)异步执行f
if (fut.wait_for(0) == std::future_status::deferred) // 如果工作被推延
{
... // fut应用get或wait来同步调用f
} else { // 工作没有被推延
while(fut.wait_for(100ms) != std::future_status::ready) { // 不可能有限循环
... // 工作没有被推延也没有就绪,所以做一些并发的事件直到工作就绪
}
... // fut就绪
}
有人可能会说既然有这么多毛病为啥还要用它,因为毕竟咱们思考的极限状况下的可能,有时候我不要求它是并发还是同步执行,也不须要思考批改那个线程thread_local变量,同时也能承受可能工作永远不会执行,那么这种形式就是一种不便且高效的调度策略。
综上所述,咱们总结出以下几点:
- std::async的默认调度策略既容许工作异步执行,又容许工作同步执行。
- 默认策略灵活性导致了应用thread_local变量时的不确定性,它隐含着工作可能不会执行,它还影响了基于超时的wait调用的程序逻辑。
- 如果异步执行是必须的,指定std::launch::async发射策略。
参考文章:
API Reference Document
用C++11的std::async代替线程的创立
发表回复