在多线程环境中,不管是传递lambda还是传递函数指针,再或者是传递函数对象给std::thread,都很难获取执行函数返回值。在以前,只能将结果以引用的形式作为线程函数参数的一部分以此保存返回值,但是仍然存在很大局限性,甚至不太美观。C++11引入的std::future可以有效解决这一问题。

std::future定义在头文件<future>中,提供了一种获取异步操作返回值的机制,不过通常与下列三个配合使用

  • std::promise
  • std::packaged_task
  • std::async

这三个操作各有不同,但是都有一个共同点就是都提供了get_future接口用于获得与之关联的future,使用者(主线程)可以通过返回的future获得异步操作结果。

std::promise

简单来说,promise是一种用于消息传递的机制,或者说是提供存储值和异常的设施。当创建线程时可以将promise引用传给线程函数,当在线程函数(异步操作)中计算得知了主线程想要的结果后通过promise::set_value*等接口设置值(如果出现异常也可以设置异常)。而主线程可以通过从promise获取的future获取结果

示例:利用std::future和std::promise实现并发std::find函数

和并发std::accumulate的实现类似,首先计算合适的线程数,将给定区间拆分成若干小区间,并行执行查找操作,当找到结果后,通过std::promise设置查找结果,而主线程则通过std::future获取结果

#include <future>
#include <thread>
#include <vector>
#include <algorithm>
#include <cassert>namespace parallel
{template <class InputIt, class T>InputIt find(InputIt first, InputIt last, const T& value){/* * 计算合适的线程数* std::thread::hardware_concurrency()用于返回当前系统支持的并发数*/auto count = std::distance(first, last);auto avaThreadNums = std::thread::hardware_concurrency();auto perThreadMinNums = 20;auto maxThreadNums = ((count + (perThreadMinNums - 1)) & (~(perThreadMinNums - 1))) / perThreadMinNums;auto threadNums = avaThreadNums == 0 ? maxThreadNums : std::min(static_cast<int>(maxThreadNums), static_cast<int>(avaThreadNums));auto blockSize = count / threadNums;/* 主线程创建std::promise实例,模板参数是返回值类型 */std::promise<InputIt> result;/* 因为不同线程会并发查找,当一个线程找到后其他线程就可以停止查找了,原子变量done用于标记是否找到 */std::atomic<bool> done(false);{std::vector<std::thread> threads;auto front = first;for(int i = 0; i < threadNums; ++i){auto back = front;if(i != threadNums - 1)std::advance(back, blockSize);elseback = last;threads.emplace_back([front, back, &value, &result, &done]{/* 当一个线程找到后所有线程都会退出,通过done标记管理 */for(auto it = front; !done && it != back; ++it){if(*it == value){done.store(true);/* 如果找到,记录找到的值 */result.set_value(it);return;}}});}/* 回收线程资源 */for(auto &th : threads)th.join();}/* 通过std::promise::get_future获得std::future对象,然后调用get获取结果 */return done ? result.get_future().get() : last;}
}int main()
{std::vector<int> v(100000000);int n = 0;std::generate(v.begin(), v.end(), [&n] { return ++n; }); auto value = std::random_device()() % 65536;auto it1 = parallel::find(v.begin(), v.end(), value); auto it2 = std::find(v.begin(), v.end(), value);assert(it1 == it2);return 0;
}

本例中同时并发了多个线程执行find操作,而最后只需要获取找到结果的那个线程返回的值,不管哪个线程找到结果,都可以记录在std::promise实例中,最终通过std::future返回

当然,使用std::promise的做法和给线程函数传入引用记录结果的做法基本相同,不过std::promise的功能不仅仅局限于此,使用起来也更加容易,结构更加清晰

std::packaged_task

std::packaged_task用于包装任何可调用对象,无非就是函数指针,函数对象,lambda等,功能类似于std::function,但是packaged_task可以通过返回的future获取异步操作的结果。

举个例子,当存在一个函数,而这个函数通常会被其它线程执行时,那么想要获取这个函数的返回值就是件困难的事情,以std::function为例,假设在一个线程池中,主线程通过std::function包装了一个函数,添加到任务队列中,随后线程池中其它线程取出这个任务函数并开始执行,在这种情况下,主线程是很难获取这个函数的返回值的。换做std::packaged_task就不同了,它可以通过get_future接口获取std::future实例,正如先前所说,std::future用于获取异步操作的结果,所以无论函数由谁执行,都可以通过std::future::get接口获取返回值

示例:利用std::packaged_task实现向线程池中添加任务

在介绍std::thread的那一篇中,涉及到了线程池的实现,借着对std::packaged_task的理解,重新实现一下向任务队列中添加任务的函数,同时需要确保调用者能够获取任务函数返回的结果,这里可以返回给调用者一个std::future实例。另外,获取std::future实例有三种方法,其中涉及到函数包装的是std::packaged_task,所以在添加任务时,将任务函数包装在packaged_task中,返回future

template <class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args... args)-> std::future<typename std::result_of<F(Args...)>::type>
{/* 获取函数f的返回结果,因为std::future模板参数需要保存结果类型 */using return_type = typename std::result_of<F(Args...)>::type;/* std::packaged_task不允许复制,所以用指针保存 *//* std::bind()返回可调用对象,包装在packaged_task中 */auto task = std::make_shared<std::packaged_task<return_type()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...));/* 获取future,用于获得执行结果 */std::future<return_type> result = task->get_future();{std::unique_lock<std::mutex> lock(mutex_);tasks_.push([task] { (*task)(); });cond_.notify_one();}/* 返回future */return result;
}int main()
{ThreadPool pool(4);std::vector<std::future<int>> results;for(int i = 0; i < 10; ++i){results.emplace_back(pool.enqueue([i]{return i * i;}));}for(auto&& result : results)std::cout << result.get() << std::endl;return 0;
}

std::async

对于std::async而言,感觉它的抽象要深一些,std::async用于异步执行给定函数,并返回用于获取函数返回值的std::future实例。所以std::async本质上应该是开启一个线程执行给定函数,内部采用std::packaged_task对函数进行包装,然后返回std::future

std::async构造函数有一个异步属性,分别是

  • std::launch::async,表示立即开启异步求值
  • std::launch::deferred,延迟开启,只有当返回的future实例调用get函数时才开启异步求值

而默认情况下的异步属性是std::launch::async | std::launch::deferred,所以到底是立即开启还是延迟开启取决于编译器的不同。如果异步至关重要的话记得在构造函数中指定std::launch::async

示例:利用std::async实行并行std::for_each函数

std::for_each会对指定区间的每一个元素执行给定的函数,所以完全可以并行化。

template <class InputIt, class UnaryFunction>
UnaryFunction for_each(InputIt first, InputIt last, UnaryFunction f)
{auto count = tinystl::distance(first, last);if(!count)  return f;if(count <= 100){tinystl::for_each(first, last, f);}else{auto middle = first;tinystl::advance(middle, count / 2);/* 开启异步操作对后半部分执行for_each */std::async(std::launch::async, tinystl::parallel::for_each<InputIt, UnaryFunction>, middle, last, f);/* 当前线程执行前半部分 */tinystl::for_each(first, middle, f);}return f;
}

小结

std::future提供了获取异步操作执行结果的机制,std::promise用于保存值和异常,可以看成是消息传递的一种,std::packaged_task用于对可调用对象的保证,std::async会开启一个异步操作,效果等同于创建新线程(或将执行函数添加到线程池),包装线程函数,返回future实例

C++11学习笔记-----获取异步操作执行结果相关推荐

  1. [python教程入门学习]python学习笔记(CMD执行文件并传入参数)

    本文章向大家介绍python学习笔记(CMD执行文件并传入参数),主要包括python学习笔记(CMD执行文件并传入参数)使用实例.应用技巧.基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋 ...

  2. main 函数解析(二)—— Linux-0.11 学习笔记(六)

    main函数解析(二)--Linux-0.11 学习笔记(六) 4.6 blk_dev_init函数 void blk_dev_init(void) {int i;for (i=0 ; i<NR ...

  3. setup.s 分析—— Linux-0.11 学习笔记(二)

    更新记录 版本 时间 修订内容 1.0 2018-4-14 增加了"获取显示模式"这一节,AL取值的表格 标题: setup.s 分析-- Linux-0.11 学习笔记(二) 老 ...

  4. main函数解析(一)——Linux-0.11 学习笔记(五)

    main()函数解析(一)--Linux-0.11 学习笔记(五) 经过了前面的各种铺垫,终于来到了main函数.这篇博客的任务是把init/main.c讲清楚.由于牵扯到很多的函数调用,要想一次就说 ...

  5. kernel_mktime() 详解 —— Linux-0.11 学习笔记(四)

    题目:kernel_mktime() 详解 -- Linux-0.11 学习笔记(四) 在init/main.c文件中,有一个函数static void time_init(void) 该函数读取 C ...

  6. Hank的无线802.11学习笔记--part 5

    Technorati 标签: 802.11,无线,wifi,原理,基础 还需要了解一些无线技术指标: 无线电频率(Hz) 无论是发射天线还是接收天线,它们总是在一定的频率范围内工作的,通常,工作在中心 ...

  7. 树莓派学习笔记——获取树莓派CPU温度

    0 前言 本文通过文件操作读取树莓派CPU温度,在linux系统中任何设备的操作都被抽象成为文件读写,通过读取/sys/class/thermal/thermal_zone0/temp文件中的内容便获 ...

  8. C++11 学习笔记(持续更新)

    今天是2021年的第一天,立个新年的第一个flag,要在1月1日~2月15日过一遍<C++ Primer Plus>和<Effective C++>,并做好笔记,写好blog, ...

  9. mysql 拼接sql批量执行_Mysql 学习笔记之 SQL 执行过程

    写在开始 本系列源自极客时间 MySQL 专栏,整理而成 在执行下面这个查询语句时的执行的流程是怎么样的? mysql 看过相关资料的同学都可能知道执行流程大概是这样的: 其执行过程为:连接.查询缓存 ...

最新文章

  1. 玩转ios友盟远程推送,16年5月图文防坑版
  2. java环境变量javac不能成功 win7_Java开发:Java环境搭建
  3. framebuffer的入门介绍-实现程序分析【转】
  4. MariaDB 求和,最大值,最小值,平均数
  5. 最大化窗口设置_BetterTouchTool的几个实用设置
  6. java无锁消费者框架_无锁并行框架多生产者多消费者模型
  7. shell开启飞行模式_原来手机飞行模式有这么多用处!99%的深圳人都不知道...
  8. springboot aop使用_SpringBoot 使用AOP实现读写分离
  9. HDU2503 a/b + c/d【水题】
  10. Log4net 配置实例
  11. Python语言程序设计 第七周 文件和数据格式化
  12. 图片批量转换为base64
  13. 小米笔试题 风口的猪-中国牛市
  14. Linux克隆后修改IP
  15. 淘宝经典移动轮播制作
  16. Linux无线网卡驱动更新
  17. 服务器匹配原理,王者荣耀实现原理学习笔记
  18. android 气泡,Android Q 气泡
  19. Siemens PLC S7-1500 AES 加,解密算法
  20. 微软发布6月份安全更新程序补丁(For Windows xp/2003/2000/vista和Office 2003/2007所有更新产品补丁)

热门文章

  1. idea git 过滤target_IDEA + maven 零基础构建 java agent 项目
  2. Java黑皮书课后题第5章:*5.47(商业:检测ISBN-13)ISBN-13是标识书籍的新标准。它使用13位数字d1d2d3~d12d13,d13是校验和。如果校验和为10,则替换为0。求所有数字
  3. vue 创建项目的命令
  4. error CS1002: ; expected 错误解决
  5. JDBC 创建连接对象的三种方式 、 properties文件的建立、编辑和信息获取
  6. (11.06)Java小知识
  7. XML4跨浏览器兼容
  8. 【Treap】[BZOJ 3224]Tyvj 1728 普通平衡树
  9. 关于Thinkphp3.2版本的分页问题
  10. 《计算机组成与体系结构:性能设计》读后小记 4、cache存储器