C++ 多线程(future篇)

news2025/1/17 3:13:25

引言

       在前面介绍了启动线程,以及多线程下如何保证共享资源的竞争访问、线程同步这些。但是thread类无法访问从线程直接返回的值,如果要想获取线程的的执行结果,一般都是依靠全局或static变量,或是以实参传递的变量,然后结合互斥锁、条件变量,等待的线程去查验所等待的条件。假如某个线程按计划只等待一次,只要条件成立一次,它就不再理会条件变量了,条件变量不一定就是这种同步模式的最佳选择, 如果我们等待的条件是判定某份数据是否可用,C++ 标准库提供的std::future类模板更适合这种场景。

         C++ 标准库提供了std::future类模板来获取异步任务(即在单独的线程中启动的函数)的返回结果,并捕捉其所抛出的异常,这种获取结果的方式是异步的。如果线程需要等待某个特定的一次性事件发生,则会以恰当的方式取得一个future,它代表目标事件;接着这个该线程可以一边执行任务,一边在future上等待;同时,它以短暂的间隔反复查验目标事件是否已经发生。这个线程也可以转换运行模式,先不等目标事件发生,直接暂缓当前任务,而切换到别饿任务,到了必要时,才回头等待futere准备就绪。future可能与数据关联,也可能未关联。一旦目标事件发生,其future即进入就绪状态,无法重置 。    

std::future

      C++标准库使用std::future为一次性事件建模,如果一个事件需要等待特定的一次性事件,那么这线程可以获取一个future对象来代表这个事件。异步调用往往不知道何时返回,但是如果异步调用的过程需要同步,或者说后一个异步调用需要使用前一个异步调用的结果。这个时候就要用到future。线程可以周期性的在这个future上等待一小段时间,检查future是否已经ready,如果没有,该线程可以先去做另一个任务,一旦future就绪,该future就无法复位(无法再次使用这个future等待这个事件),所以future代表的是一次性事件。       

         std::future是一个类模板(class template),其对象存储未来的值,从一个异步调用的角度来说,future更像是执行函数的返回值,其模板参数就是期待返回的类型。



future类模板

template<typename ResultType>
class future
{
public:
  future() noexcept;
  future(future&&) noexcept;
  future& operator=(future&&) noexcept;
  ~future();
  
  future(future const&) = delete;
  future& operator=(future const&) = delete;


  bool valid() const noexcept;
  
  ResultType get();
  shared_future<ResultType> share();

  void wait();

  template<typename Rep,typename Period>
  future_status wait_for(
      std::chrono::duration<Rep,Period> const& relative_time);

  template<typename Clock,typename Duration>
  future_status wait_until(
      std::chrono::time_point<Clock,Duration> const& absolute_time);
};
成员函数说明
构造函数

(1).不带参数的默认构造函数,此对象没有共享状态,因此它是无效的,但是可以通过移动赋值的方式将一个有效的future值赋值给它;

(2).禁用拷贝构造;

(3).支持移动构造

析构函数
operator=

移动future对象 (公开成员函数)
(1).禁用拷贝赋值。

(2).支持移动赋值:如果在调用之前,此对象是有效的(即它已经访问共享状态),则将其与先前已关联的共享状态解除关联。如果它是与先前共享状态关联的唯一对象,则先前的共享状态也会被销毁

share从 *this 转移共享状态给 shared_future 并返回它 (公开成员函数)
get()

返回结果 (公开成员函数)

(1).当共享状态就绪时,返回存储在共享状态中的值(或抛出异常)。

(2).如果共享状态尚未就绪(即提供者尚未设置其值或异常),则该函数将阻塞调用的线程直到就绪。

(3).当共享状态就绪后,则该函数将取消阻塞并返回(或抛出)释放其共享状态,这使得future对象不再有效,因此对于每一个future共享状态,该函数最多应被调用一次。(4).std::future<void>::get()不返回任何值,但仍等待共享状态就绪并释放它。

(5).共享状态是作为原子操作(atomic operation)被访问

valid()检查 future 是否拥有共享状态(公开成员函数)
wait()

等待结果变得可用

(1).等待共享状态就绪。

(2).如果共享状态尚未就绪(即提供者尚未设置其值或异常),则该函数将阻塞调用的线程直到就绪。

(3).当共享状态就绪后,则该函数将取消阻塞并void返回

wait_for()等待结果,如果在指定的超时间隔后仍然无法得到结果,则返回。
wait_until()等待结果,如果在已经到达指定的时间点时仍然无法得到结果,则返回。 

std::future的类型

 在库的头文件中声明了两种future,唯一future(std::future<>)和共享future(std::shared_future<>)这两个是参照std::unique_ptr和std::shared_ptr设立的,

  • 唯一future(std::future<>)

仅有一个指向其关联事件的实例,

  • 共享future(std::shared_future<>)

共享future(std::shared_future<>)可以有多个实例指向同一个关联事件,当事件就绪时,所有指向同一事件的std::shared_future实例会变成就绪。

总之,类模板 std::future 提供访问异步操作结果的机制:

1. 提供一个 std::future 对象给异步操作的创建者,一个有效的std::future对象通常是由某个 Provider 创建,你可以把 Provider 想象成一个异步任务的提供者,通常是:

  • std::async() 函数
  • std::promise::get_future(),get_future() 为 promise 类的成员函数
  • std::packaged_task::get_future(),此时 get_future()为 packaged_task的成员函数

      由 std::future 默认构造函数创建的 future 对象不是有效的(除非当前非有效的 future 对象被 move 赋值另一个有效的 future 对象)

2. Provider 在某个线程中设置共享状态的值,与该共享状态相关联的 std::future 对象调用 get(通常在另外一个线程中) 获取该值,如果共享状态的标志不为 ready,则调用 std::future::get 会阻塞当前的调用者,直到 Provider 设置了共享状态的值(此时共享状态的标志变为 ready),std::future::get 返回异步任务的值或异常(如果发生了异常)。

std::async()

        因为std::thread没有提供直接回传结果的方法,函数模板std::async()应运而生。只要我们并不着急需要线程运算的结果,就可以用std::async()按异步方式启动任务。我们从std::async()函数处获得std::future对象(而非thread对象),运行的函数一旦完成,其返回值就有该对象最后持有。若要用到这个值,只需要在future对象上调用get(),当前线程就会阻塞,以便future准备完毕并返回这个值.

std::async函数原型

template<class Fn, class... Args>

future<typename result_of<Fn(Args...)>::type> async(launch policy, Fn&& fn, Args&&...args);

参数描述
fn

任务函数(仿函数、lambda表达式、类成员函数、普通函数……)

1.如果是要异步运行某个类的某个成员函数

任务函数这个参数应该是一个函数指针,指向该类的目标成员函数;

任务函数这个参数需要给出相应的对象,以在它之上调用成员函数(这个参数可以是指向对象的指针,或对象本身,或ref包装的对象)

余下的async()的参数会传递给成员函数,用作成员函数的参数

2. 如果运行的是普通函数

第一个参数是指定任务函数(或目标可调用对象),其参数取自async()里余下的参数。

policy

决定异步执行,还是同步执行任务

  1. std::launch::async 异步执行传递的任务函数,必须另外开启专属的线程去运行任务函数。
  2. std::launch::deferred 同步执行传递的任务函数,在当前线程上延后调用任务函数,等到了在future上调用了wait()或get(),任务函数才会执行,即函数调用被延迟。
  3. std::launch::async | std::launch::deferred 可以异步或是同步,取决于操作系统,我们无法控制;
  4. 如果我们不指定策略,则相当于3。

下面演示了用launch::async和launch::deferred两种不同的policy执行任务函数的差别,任务函数都是返回tid。从运行结果看得出来,使用async policy的方式是另起了一个thread运行的任务函数,因为其tid和调用线程的tid不同;相反,使用deferred policy的调用线程的tid和运行任务函数的tid是相等的,说明是在调用线程里调用的任务函数,而没有单独起一个线程去做。

#include <iostream>
#include <thread>
#include <future>

using namespace std;

thread::id  test_async() {
	this_thread::sleep_for(chrono::milliseconds(500));
	return this_thread::get_id();
}

thread::id  test_async_deferred() {
	this_thread::sleep_for(chrono::milliseconds(500));
	return this_thread::get_id();
}

int main() {
	future<thread::id> ans = std::async(launch::async, test_async); //另起一个线程去运行test_async
	future<thread::id> ans_def = std::async(launch::deferred,test_async_deferred); //还没有运行test_async_deferred

	cout << "main thread id = " << this_thread::get_id() << endl;
	cout << "test_async thread id = " << ans.get() << endl;//如果test_async这时还未运行完,程序会阻塞在这里,直到test_async运行结束返回
	cout << "test_async_deferred thread id =  = " << ans_def.get() << endl;//这时候才去调用test_async_deferred,程序会阻塞在这里,直到test_async_deferred运行返回

	return 0;
}

std::packaged_task

         package_task<>是一个类模板,packaged_task类把一个可调用目标(函数、lambda表达式、bind表达式、函数对象)包装成一个对象,以便它可以被异步调用。packaged_task它连结了future对象与函数,package_task<>对象在执行任务时,会调用关联的函数,把返回值保存为future的内部数据,并令future准备就绪。

         package_task<>其模板参数是函数签名:比如,void()表示一个函数,不接收参数,也不接收返回值;int(string&,double*)代表某函数,它接收两个参数并返回int值。假设我们要构建packaged_task<>实例,那么,由于模板参数先行指定了函数签名,因此传入的函数必须与之相符。即它应该接收指定类型的参数,返回值也必须可以转换为指定类型。

  1. 类模板packaged_task<>具有成员函数get_future,它返回future<>实例,该future的特化类型取决于函数签名所指定的返回值
  2. packaged_task<>还具备函数调用操作符,它的参数取决于函数签名的参数列表。

packaged_task对象是可调用对象,我们可以直接调用,还可以将其包装在function对象内,当作线程函数传递给thread对象,也可以传递给需要可调用对象的函数。如果packaged_task作为函数对象而被调用,它就会通过函数调用操作符接收参数,并将其进一步传递给包装在内的任务函数,由其异步运行得出结果,并将其结果保存到future对象内部,再通过get_future()获取此对象。所以,为了在未来的适当时刻执行某项任务,我们可以将其包装在packaged_task对象内,取得对应的future之后,才把该对象传递给其他线程,由它触发任务执行。等到需要用到使用结果时,我们静候future准备就绪即可。

#include <iostream>
#include <thread>
#include <future>

using namespace std;

int add(int a,int b) {
	cout <<"sub thread id: " << this_thread::get_id() << endl;
	return a + b;
}

int main() {
	cout << "main thread id: " << this_thread::get_id() << endl;
	packaged_task<int(int, int)> task_add(add);
	future<int> res = task_add.get_future();

	//另起线程调用 
	thread th(move(task_add),100,500);
	
	//也可以直接调用,和调用线程处于同一线程
    //task_add(100,500);

	cout << "res = " << res.get() << endl;

	th.join();
	return 0;
}

std::promise

有些任务无法以简单的函数调用表达出来,还有一些任务的执行结果可能来自多个部分的代码。promise<T>给出了一种异步求值的方法,某个future<T>对象与结果关联,能延后读出需要求取的值。配对的promise和future可实现下面的工作机制:等待数据的线程在future上阻塞,而提供数据的线程利用相配的promise设定关联的值,使future准备就绪。 

如下这段程序,创建一个 promise 对象,然后通过 promise 对象获取一个 future 对象,promise 在一个线程中设置了一个值,将 future 对象传递到另一个线程中去,而另一个线程中可以通过 std::future 来访问这个值(或者异常)。

#include <iostream>
#include <thread>
#include <future>

using namespace std;

void add(int a, int b, promise<int>& p) {
	p.set_value(a + b);
}

void useResult(future<int>& f) {
	int val = f.get();//阻塞函数,直到收到相关联的std::promise对象传入的数据
	cout << "useResult: val = " << val << endl;
}

int main() {
	promise<int> prom;
	future<int> fut = prom.get_future();

	thread th(add,100,90,ref(prom));
	thread th2(useResult,ref(fut));

	th.join();
	th2.join();
	return 0;
}

       promise 提供了一个承诺(promise),表示在某个时间点一定会有一个值或一个异常会被设置。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/998006.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C# 辗转相除法求最大公约数

辗转相除法求最大公约数 public static void CalcGCD(int largeNumber, int smallNumber, out int GCD){GCD 1;int remain -1;while (remain ! 0){remain largeNumber % smallNumber;GCD smallNumber;largeNumber smallNumber;smallNumber remain;}}

华为云云耀云服务器L实例评测|华为云耀云L搭建zerotier服务测试

0. 环境 - Win10 - 云耀云L服务器 1. 安装docker 检查yum源&#xff0c;本EulerOS的源在这里&#xff1a; cd /etc/yum.repos.d 更新源 yum makecache 安装 yum install -y docker-engine 运行测试 docker run hello-world 2. 运行docker镜像 默认配…

计算机专业毕业设计项目推荐03-Wiki系统设计与实现(JavaSpring+Vue+Mysql)

Wiki系统设计与实现&#xff08;JavaSpringVueMysql&#xff09; **介绍****系统总体开发情况-功能模块****各部分模块实现** 介绍 本系列(后期可能博主会统一为专栏)博文献给即将毕业的计算机专业同学们,因为博主自身本科和硕士也是科班出生,所以也比较了解计算机专业的毕业设…

大秒杀系统设计

参考链接&#xff1a;http://www.taodudu.cc/news/show-5770725.html?actiononClick 1. 一些数据 大家还记得2013年的小米秒杀吗&#xff1f;三款小米手机各11万台开卖&#xff0c;走的都是大秒系统&#xff0c;3分钟后成为双十一第一家也是最快破亿的旗舰店。 经过日志统计…

[超硬核] 5000字带走读DuckDB优化器之常量折叠与比较简化

DuckDB优化器之常量折叠与比较简化 本篇文章适合学习C的小伙伴&#xff0c;适合阅读开源项目的小伙伴&#xff0c;更适合学习数据库的小伙伴&#xff0c;欢迎与我一起探索优化器知识。 目录 DuckDB优化器之常量折叠与比较简化1.优化器规则2.表达式重写 2.1 重写/访问算子2.2 应…

makefile之目标文件生成

目标文件:源码经过编译还没有链接那些中间文件.linux .o文件 gcc $(CFLAGS) -c xxx.c -o xx.o include Makefile.config SRC : $(wildcard *.c wildcard ./audio_module/*.c) SRC_OBJ $(patsubst %.c,%.o,$(SRC))all:$(SRC_OBJ) $(info contents $(SRC))$(info objfiles $(SR…

Tomcat部署与调优

一、Tomcat概述&#xff1a; Tomcat服务器是一个免费的开放源代码的Web应用服务器&#xff0c;属于轻量级应用服务器&#xff0c;在中小型系统和并发访问用户不是很多的场合下被普遍使用&#xff0c;具有处理HTML页面的功能&#xff0c;然而由于其处理静态HTML的能力远不及Apac…

81 # 多语言

多语言实现方案 1、一个完整多个路径来实现多语言 2、前端来实现多语言&#xff08;先配置好两种语言&#xff0c;动态切换内容&#xff09;&#xff0c;比如 i18n&#xff0c;vue-i18n 3、服务端的 header 来实现切换多语言 accept-language: zh-CN,zh;q0.9 const fs req…

台式万用表几位的概念以及NPLC的功能作用

数字万用表测量电流和电压的基本原理是通过检测传感器的电阻&#xff0c;电容&#xff0c;或电感等特性&#xff0c;将电流或电压转化为可以测量的电信号&#xff0c;然后这个电信号被转化为数字信号进行处理和显示。具体的&#xff0c;当测量电压时&#xff0c;万用表的输入端…

【SpringMVC】Jrebel 插件实现热部署与文件上传

目录 一、JRebel 1.1 Jrebel介绍 1.2 Jrebel插件下载 1.3 Jrebel服务下载并启动 1.4 在线生成GUID 1.5 JRebel激活 1.6 相关设置 注意❗ 二、文件上传、下载 2.1 导入pom依赖 2.2 配置文件上传解析器 2.3 文件上传表单设置 2.4 文件上传实现 2.5 文件下载实现 2…

[源码系列:手写spring] IOC第十四节:容器事件和事件监听器

代码分支 https://github.com/yihuiaa/little-spring/tree/event-and-event-listenerhttps://github.com/yihuiaa/little-spring/tree/event-and-event-listener 内容介绍 事件监听器机制 Spring的容器事件和事件监听器机制允许应用程序在容器中发生特定事件时执行自定义逻辑…

Hadoop的HDFS的集群安装部署

注意&#xff1a;主机名不要有/_等特殊的字符&#xff0c;不然后面会出问题。有问题可以看看第5点&#xff08;问题&#xff09;。 1、下载 1.1、去官网&#xff0c;点下载 下载地址&#xff1a;https://hadoop.apache.org/ 1.2、选择下载的版本 1.2.1、最新版 1.2.2、其…

SQL数据库查询超时,查询数据库的哪些表被上锁的语句

1.异常提示 2.表语句 2.1 查询锁表的语句 select request_session_id spid,OBJECT_NAME(resource_associated_entity_id) tableName from sys.dm_tran_locks where resource_typeOBJECT * 若是下面没有显示内容&#xff0c;说明当前没有锁住的表 2.2若是有显示锁住的表&#…

【实践篇】Redis最强Java客户端(一)之Redisson入门介绍

Redisson入门介绍 文章目录 Redisson入门介绍1.1 Redisson简介1.1.1 起源和历史1.1.2 优势和特点1.1.3 与其他Java Redis客户端的比较 1.2 使用和配置1.2.1 依赖和SDK1.2.2 配置文件解析1.2.3 连接池配置 1.3 优雅的让Hash的某个Field过期2. 参考资料3. 源码地址4. Redis从入门…

9. xaml ComboBox控件

1.运行图像 2.运行源码 a.Xaml源码 <Grid Name="Grid1"><!--IsDropDownOpen="True" 默认就是打开的--><ComboBox x:Name="co

flink的网络缓冲区

背景 在flink的taskmanager进行数据交互的过程中&#xff0c;网络缓冲区是一个可以提升网络交换速度的设计&#xff0c;此外&#xff0c;flink还通过网络缓冲区实现其基于信用值credit的流量控制&#xff0c;以便尽可能的处理数据倾斜问题 网络缓冲区 在flink中每个taskmana…

Jetsonnano B01 笔记6:开启USB摄像头

今日继续我的Jetsonnano学习之路&#xff0c;今日尝试开启一下USB摄像头&#xff0c;显示拍摄的内容。 测试代码是搬运的官方说明&#xff0c;这里只是作笔记记录学习&#xff1a; 目录 额外模块准备&#xff1a; 测试代码分析&#xff1a; 运行效果&#xff1a; 额外模块准…

【Windows】磁盘管理无法删除卷

磁盘管理无法删除卷 由于HP Cloud Recovery Tool在对U盘分区时出现闪退 尝试在Windows磁盘管理中使U盘恢复“未分配状态” 右键删除卷出现报错 虚拟磁盘管理器&#xff1a;不支持该请求 ✨解决方案 使用diskpart命令行工具 在Terminal运行如下命令行 diskpart # 列出所有…

C++ day 3

1、 自行封装一个栈的类&#xff0c;包含私有成员属性&#xff1a;栈的数组、记录栈顶的变量&#xff0c;成员函数完成&#xff1a;构造函数、析构函数、拷贝构造函数、入栈、出栈、清空栈、判空、判满、获取栈顶元素、求栈的大小 stack.h #ifndef STACK_H #define STACK_H#…

业务安全详解

文章目录 一、 业务安全概述1.1 业务安全现状1.1.1 业务逻辑漏洞1.1.2 黑客攻击的目标 二、 业务安全测试2.1 业务安全测试流程2.1.1 测试准备2.1.2 业务调研2.1.3 业务建模2.1.4 业务流程梳理2.1.5 业务风险点识别2.1.6 开展测试2.1.7 撰写报告 三、 业务安全经典场景3.1 业务…