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.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代替线程的创建

std::async的使用总结的更多相关文章

  1. 用C++11的std::async代替线程的创建

    c++11中增加了线程,使得我们可以非常方便的创建线程,它的基本用法是这样的: void f(int n); std::thread t(f, n + 1); t.join(); 但是线程毕竟是属于比 ...

  2. C++并发高级接口:std::async和std::future

    std::async和std::future std::async创建一个后台线程执行传递的任务,这个任务只要是callable object均可,然后返回一个std::future.future储存 ...

  3. C++11 使用异步编程std::async和std::future

    先说明一点:std::asyanc是std::future的高级封装, 一般我们不会直接使用std::futrue,而是使用对std::future的高级封装std::async. 下面分别说一下. ...

  4. C++ std::async vs async/await in C# - Stack Overflow

    C++ std::async vs async/await in C# - Stack Overflow 我想知道新的c ++功能std::async是否与两个C#关键字async / await相当 ...

  5. c++ 如何获取多线程的返回值?(std::thread ,std::async)

    //简单的 c++11 线程,简单方便,成员函数随便调用,非成员函数也一样,如需要获取返回时,请自行使用条件变量 std::thread run([&](){ //执行一些耗时的操作 retu ...

  6. 基于std::mutex std::lock_guard std::condition_variable 和std::async实现的简单同步队列

    C++多线程编程中通常会对共享的数据进行写保护,以防止多线程在对共享数据成员进行读写时造成资源争抢导致程序出现未定义的行为.通常的做法是在修改共享数据成员的时候进行加锁--mutex.在使用锁的时候通 ...

  7. C++11 使用 std::async创建异步程序

    c++11中增加了线程,使得我们可以非常方便的创建线程,它的基本用法是这样的: void f(int n); std::thread t(f, n + 1); t.join(); 但是线程毕竟是属于比 ...

  8. C++并发编程之std::async(), std::future, std::promise, std::packaged_task

    c++11中增加了线程,使得我们可以非常方便的创建线程,它的基本用法是这样的: void f(int n); std::thread t(f, n + 1); t.join(); 但是线程毕竟是属于比 ...

  9. (原创)用C++11的std::async代替线程的创建

    c++11中增加了线程,使得我们可以非常方便的创建线程,它的基本用法是这样的: void f(int n); std::thread t(f, n + ); t.join(); 但是线程毕竟是属于比较 ...

  10. std::async

    https://www.cnblogs.com/qicosmos/p/3534211.html https://bobjin.com/blog/c_cpp_docs/reference/en/cpp/ ...

随机推荐

  1. 关于easyii 无法退出登录的情况

    问题描述:easyii 后台原先自己就写好了退出登录,如下图所示.点击了退出登录后,页面也会自动跳转到登录的页面.但是问题是,在浏览器点击返回的时候,还是依旧能进入到后台中,退出登录根本就没有起到作用 ...

  2. 精尽Spring MVC源码分析 - HandlerExceptionResolver 组件

    该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...

  3. 使用CodeSmith编写ADO.Net三层

    说明 自本文发布日起,发现和完善了编写的CodeSmith几处代码. 故,不再直接在文章添加代码,最新代码参见GitHub https://github.com/catbiscuit/CodeSmit ...

  4. Excel 数据对比,窗口并列排序操作(xlw文件格式的由来)

    步骤1:打开Excel文件,输入一些数据 步骤2:点击视图,创建新窗口(这里就会创建一个和步骤1一抹一样的的表格,我们可以在任务栏上看到) 第三步:点击视图里面的全部重排按钮,在重拍窗口里面选择需要拍 ...

  5. (十五)、shell脚本之简单控制流结构

    一.基本的控制结构 1.控制流 常见的控制流就是if.then.else语句提供测试条件,测试条件可以基于各种条件.例如创建文件是否成功.是否有读写权限等,凡是执行的操作有失败的可能就可以用控制流,注 ...

  6. SQLServer多事务——事务嵌套

    在ERP中,偶尔会有存储过程里面继续调用存储过程的情况 其中更有一些特殊的存储过程分别都使用了存储过程,大致可以分为下面几种情况: 1.平行事务,在多个事务中,任意一个成功则提交数据库,失败则各自RO ...

  7. 什么是CDN?哪些是流行的jQuery CDN?使用CDN有什么好处?

    内容传送网络或内容分发网络(CDN)是部署在因特网上的多个数据中心的大型分布式服务器系统.CDN的目标是为具有高可 用性和高性能的最终用户提供内容. 有3个流行的jQuery CDN:谷歌,微软jQu ...

  8. HTML表格样式

    一.表格的表头 1 <html> 2 3 <body> 4 5 <h4>表头:</h4> 6 <table border="1" ...

  9. 前端面试题归类-HTML2

    一. SGML . HTML .XML 和 XHTML 的区别? SGML 是标准通用标记语言,是一种定义电子文档结构和描述其内容的国际标准语言,是所有电子文档标记语言的起源. HTML 是超文本标记 ...

  10. JS中var与let的区别

    区别: var声明的变量,其作用域在该语句所在的函数之内,存在着变量提升的现象. let声明的变量,其作用域为该句所在的代码块内,不存在变量提升的问题. let相比于var,其不允许在相同作用域内,重 ...