使用 C++23 协程实现第一个 co_await 同步风格调用接口--Qt计算文件哈希值

C++加入了协程 coroutine的特性,一直没有动手实现过。看了网上很多文章,已经了解了协程作为“可被中断和恢复的函数”的一系列特点。在学习过程中,我发现大多数网上的例子,要不就是在main()函数的控制台程序里演示yeild,await, resume的特性,要不就是讲述很多概念,很少有演示协程究竟如何把异步变成同步调用的。本次,我们就通过一个简单的计算文件哈希值的例子,来演示如何进行协程操作。

1. 原始的哈希值计算

假设存在一个最简单的哈希计算需求,要计算一个大文件的指纹。我们很容易实现一个演示算法:

void DlgCT::on_pushButton_normal_clicked(){
	QFile fp(filename);
	char buf[1024];
	unsigned long long hashfile = 0;
	if (fp.open(QIODevice::ReadOnly))
	{
		int rlen = fp.read(buf,1024);
		while (rlen>0)
		{
			for (int i=0;i<rlen;++i)
			{
				unsigned char c = hashfile>>56;
				hashfile <<=8;
				hashfile ^= (buf[i] ^ c );
			}
			rlen = fp.read(buf,1024);
			//假多线程,也可以看做是Qt的有栈协程,人为释放资源
			QCoreApplication::processEvents();
		}
	}
}

上面的代码,在文件比较大时,如果没有‘’QCoreApplication::processEvents();”显然会阻塞界面,导致按钮弹不起来,界面卡死。当然,可以通过适时调用QCoreApplication::processEvents();保持消息循环。这是一种假多线程,也可以看做是Qt的有栈协程,人为释放资源让给其他消息。

2. 异步计算改造

为了不阻塞主界面,传统上喜欢使用另一个线程来处理算法,并在完成后通知主线程。有一个处理类:

class fileDealer : public QObject
{
	Q_OBJECT
public:
	explicit fileDealer(QObject *parent = nullptr);
	//dealFile 计算哈希,存储在 result 里
	void dealFile(QString filename);
public:
	QByteArray result;
private:
	std::thread * m_pThread = nullptr;
signals:
	void sig_done();
};

void fileDealer::dealFile(QString filename)
{
	m_pThread = new std::thread([filename,this]()->void{		
		QFile fp(filename);
		char buf[1024];
		unsigned long long hashfile = 0;
		if (fp.open(QIODevice::ReadOnly))
		{			
			int rlen = fp.read(buf,1024);
			while (rlen>0)
			{
				for (int i=0;i<rlen;++i)
				{
					unsigned char c = hashfile>>56;
					hashfile <<=8;
					hashfile ^= (buf[i] ^ c );
				}				
				rlen = fp.read(buf,1024);
			}
		}
		emit sig_done();		
	});
}

这个类会开启一个独立的线程,做完后触发信号sig_done。上述代码是主干功能,相应的new,delete维护部分略去。如此一来,则需要在按钮响应函数里改造异步调用:



void DlgCT::on_pushButton_thread_clicked()
{
	fileDealer * dealer = new fileDealer(this);
	connect(dealer,&fileDealer::sig_done,[dealer,this]()->void{
		dealer->deleteLater();
	});
	dealer->dealFile(ui->lineEdit_file->text());
}

即可完成非阻塞处理。

3. 使用协程 co_await 同步风格编程

如果使用C++协程,当然希望直接可以实现同步风格的异步调用:

void DlgCT::on_pushButton_file_clicked()
{
	dealFile(ui->lineEdit_file->text());
}
FileTask DlgCT::dealFile(QString filename)
{
	QByteArray res = co_await awDealFile(filename);
	//注意!若协程库开发不周到,此时有可能已经不是在主界面线程了!一定注意操作界面控件的线程安全性。
	showMsg(res);
}

在 co_await 语句后,返回主消息循环,此时定时器等依旧顺利工作。直到文件计算完毕后,才返回 showMsg(res);。为了达到上述效果,需要如下两步骤:

3.1 添加协程代码

首先,添加协程返回对象结构体. 本示例只使用 co_await关键词,所以大部分的必备函数入口都是默认值,啥也不做。

/*!
 * \brief The FileTask class	协程结构体
 */
struct FileTask
{
	struct promise_type;
	using handle_type = std::coroutine_handle<promise_type>;
	FileTask(handle_type h)
	{}
	FileTask(FileTask&& s)
	{}

	struct promise_type {
		promise_type() = default;
		~promise_type() = default;
		auto get_return_object() noexcept {
			return FileTask{handle_type::from_promise(*this)};
		}
		auto initial_suspend() noexcept {
			//一创建立刻执行
			return std::suspend_never{};
		}
		auto final_suspend() noexcept {
			return std::suspend_always{};
		}
		void unhandled_exception() {
			exit(1);
		}
		void return_void()
		{}
	};

};

3.2 创建 await 辅助类

关键实现await功能的就是下面这个类:

/*!
 * \brief The awDealFile class	协程 await 对象
 */
class awDealFile : public QObject
{
	Q_OBJECT
public:
	awDealFile(QString filename, QObject *parent = nullptr)
		:QObject(parent)
		,m_fn(filename)
		,m_pDealer(new fileDealer)
	{
		//处理完毕的信号,会在处理线程里发出,所以用QueuedConnection确保协程返回时,保持线程不变。
		QObject::connect(m_pDealer,&fileDealer::sig_done,this, &awDealFile::slot_done,Qt::QueuedConnection);

	}
	~awDealFile()
	{
			if (m_pDealer)
			m_pDealer->deleteLater();
		m_pDealer = nullptr;
	}
	bool await_ready() {	return false;	}
	/*!
	 * \brief await_resume	这个函数的返回值决定了 await 关键词可以返回什么类型的东西
	 * \return 哈希结果
	 */
	QByteArray await_resume() {
		return m_pDealer->result;
	}
	/*!
	 * \brief await_suspend	co_await 时,会调用这个函数。此时,启动处理,并在处理完毕后resume
	 * \param h
	 */
	void await_suspend(FileTask::handle_type h) {
		hd = h;
		//处理
		m_pDealer->dealFile(m_fn);
	}
private slots:
	void slot_done()
	{
		if (hd)	hd.resume();
	}
private:
	QString m_fn;
	fileDealer * m_pDealer = nullptr;
	FileTask::handle_type hd;
};

有了上述代码,则可实现同步调用。

4. 关于线程切换的风险

协程的co_await 实际上提供了一个无栈的暂停-恢复框架。关键是要在确保处理完毕后,及时调用 resume 恢复执行。值得注意的是,对于从一个 std::thread内直接 resume的方法,会导致线程切换!此行为务必引起重视。在哪个线程调用的resume,协程函数恢复后,就回到哪个线程。这对操作GUI控件的代码带来了隐晦的风险!

可以看到,在例子里使用Qt的跨线程队列槽 (Qt::QueuedConnection)确保恢复后的协程执行序依旧位于主线程。虽然在实验中,多线程操作控件似乎也没有报错,但这不是推荐的控件操作方法。

	//处理完毕的信号,会在处理线程里发出,所以用QueuedConnection确保协程返回时,保持线程不变。
		QObject::connect(m_pDealer,&fileDealer::sig_done,this, &awDealFile::slot_done,Qt::QueuedConnection);

   void slot_done()
	{
		if (hd)	hd.resume();
	}

5. 范例代码

范例代码参考:

https://gitcode.net/coloreaglestdio/qtcpp_demo/-/tree/master/qt_coro_test

在 MSYS2 Qt6 /Linux下编译通过。
范例工程

6. 体会-协程用的香,协程库开发一点也不简单

上述把一个异步操作变成同步,其实就是一个语法糖,背后还是多线程。如果一下处理1000个文件,开启1000个线程是不合理的,需要管理一个线程池,并管理请求队列,保证机械硬盘在一个合理的并发规模下运转。

推而广之,协程能够发挥co_await的功效,仰赖于协程库背后的管理机制,如系统层面的异步回调(如socket)、库层面的线程池。一个简单的 co_await背后的代码量不容小觑。

比较全面的协程改造的例子,参考这个基于Qt 的协程库 https://qcoro.dvratil.cz/,可以看见为了这一句“co_await”,库开发者要做的工作。

此外,作为使用者,要搞清楚语法糖背后创建了哪些对象,生命周期如何,前后线程是不是一致,才能不踩坑。越是表面看起来无比清晰的代码,踩坑越是惊心动魄。所以如果是基于Qt这样的成熟框架,有Lambda槽回调,大可不必在生产环境激进地尝试协程。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Nginx网络服务二-----(虚拟机和location)

一、HTTP设置 1.设置虚拟主机 1.1Nginx 基于域名---虚拟主机 include /apps/nginx/conf.d/*.conf; 1.2Nginx 基于端口---虚拟主机 在做了域名的基础上&#xff0c;按照以下步骤继续 1.3Nginx 基于IP---虚拟主机 2.server下的root root路径格式 指定文件的路径 url …

1.1_1 计算机网络的概念、功能、组成和分类

文章目录 1.1_1 计算机网络的概念、功能、组成和分类&#xff08;一&#xff09;计算机网络的概念&#xff08;二&#xff09;计算机网络的功能&#xff08;三&#xff09;计算机网络的组成1.组成部分2.工作方式3.功能组成 &#xff08;四&#xff09;计算机网络的分类 总结 1.…

线性代数:向量空间

目录 向量空间 Ax 0 的解空间S Ax b 的全体解向量所构成集合不是向量空间 基、维数、子空间 自然基与坐标 例1 例2 向量空间 Ax 0 的解空间S Ax b 的全体解向量所构成集合不是向量空间 基、维数、子空间 自然基与坐标 例1 例2

使用redisMQ-spring-boot-starter实现消息队列和延时队列

简介 redisMQ-spring-boot-starter是一个轻量级的、基于Redis实现的消息队列中间件&#xff0c;它有如下优点&#xff1a; 开箱即用&#xff0c;你几乎不用添加额外的配置支持消息队列、延时队列&#xff0c;并提供精细化配置参数提供消息确认机制支持虚拟空间&#xff0c;不…

非洲数字经济持续崛起 本地化策略让传音提前入局

非洲市场&#xff0c;被誉为全球最后的“边疆级”市场&#xff0c;吸引着全球目光。近日&#xff0c;非洲开发银行最新报告指出&#xff0c;未来两年非洲的经济增长将优于世界其他地区&#xff0c;2023 年和 2024 年实际国内生产总值 (GDP) 平均约为 4%。广阔的非洲大陆焕发着勃…

PLC设置网口通讯的原因

PLC设置网口通讯功能&#xff0c;是现场总线向工业以太网的迈进&#xff0c;更是为了在网口之上构建更为庞大、开放的大一统的生态系统。 有了以太网&#xff0c;特别是TCP/IP协议后&#xff0c;全员工控人的日常工作更为便利了。 主要体现在以下4点&#xff1a; 1.再也不需要…

TiDB 社区智慧合集丨TiDB 相关 SQL 脚本大全

非常感谢各位 TiDBer 在之前 【TiDBer 唠嗑茶话会 48】非正式 TiDB 相关 SQL 脚本征集大赛&#xff01;( https://asktug.com/t/topic/996635 )里提供的各种常用脚本。 在这篇文章中&#xff0c;我们整理了社区同学提供的一系列 TiDB 相关 SQL 脚本&#xff0c;希望能为大家在…

基于springboot+vue的车辆管理系统(前后端分离)

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战&#xff0c;欢迎高校老师\讲师\同行交流合作 ​主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Pyt…

【云动世纪:Apache Doris 技术之光】

本文节选自《基础软件之路&#xff1a;企业级实践及开源之路》一书&#xff0c;该书集结了中国几乎所有主流基础软件企业的实践案例&#xff0c;由 28 位知名专家共同编写&#xff0c;系统剖析了基础软件发展趋势、四大基础软件&#xff08;数据库、操作系统、编程语言与中间件…

短视频新媒体的福音:视频抽插帧AI效率是人工的100倍以上

进入全民短视频时代&#xff0c;人像视频的拍摄也正在迈向专业化。随着固化审美的瓦解&#xff0c;十级磨皮的网红滤镜被打破&#xff0c;多元化的高级质感成为新的风向标&#xff0c;“美”到每一帧是人们对动态视频提出的更高要求。 目前&#xff0c;大部分手机均可记录主流的…

【Vuforia+Unity】AR05-实物3D模型识别功能实现(ModelTarget )

不管是什么类型的识别Vuforia的步骤基本都是&#xff1a; 把被识别的物体转成图、立体图、柱形图&#xff0c;3D模型、环境模型&#xff0c;然后模型生成Vuforia数据库-导入Unity-参考模型位置开始摆放数字内容&#xff0c;然后参考模型自动隐藏-发布APP-识别生活中实物-数字内…

体验LobeChat搭建私人聊天应用

LobeChat是什么 LobeChat 是开源的高性能聊天机器人框架&#xff0c;支持语音合成、多模态、可扩展的&#xff08;Function Call&#xff09;插件系统。支持一键免费部署私人 ChatGPT/LLM 网页应用程序。 地址&#xff1a;https://github.com/lobehub/lobe-chat 为什么要用Lobe…

【Python笔记-设计模式】工厂模式

一、说明 (一) 解决问题 提供了一种方式&#xff0c;在不指定具体类将要创建的情况下&#xff0c;将类的实例化操作延迟到子类中完成。可以实现客户端代码与具体类实现之间的解耦&#xff0c;使得系统更加灵活、可扩展和可维护。 (二) 使用场景 希望复用现有对象来节省系统…

C# cass10 宗地初始化-根据 “预编号” “权利人”图层对应信息 批量添加到宗地图层

运行环境Visual Studio 2022 c# cad2016 cass10 根据 “预编号” “权利人”图层对应信息 批量添加到宗地图层 一、主要步骤 zdimport 方法&#xff1a;这个方法用于导入宗地信息。首先通过调用 AutoCAD API 获取当前活动文档、数据库和编辑器对象。然后根据 CreatePalette.Se…

Web3 基金会推出去中心化之声计划:投入高额 DOT 和 KSM ,助力去中心化治理

作者&#xff1a;Web3 Foundation Team 编译&#xff1a;OneBlock 原文&#xff1a;https://medium.com/web3foundation/decentralized-voices-program-93623c27ae43 Web3 基金会为 Polkadot 和 Kusama 创建了去中心化之声计划&#xff08;Decentralized Voices Program&…

【深度学习笔记】3_1 线性回归

注&#xff1a;本文为《动手学深度学习》开源内容&#xff0c;仅为个人学习记录&#xff0c;无抄袭搬运意图 3.1 线性回归 线性回归输出是一个连续值&#xff0c;因此适用于回归问题。回归问题在实际中很常见&#xff0c;如预测房屋价格、气温、销售额等连续值的问题。与回归问…

3.测试教程 - 基础篇

文章目录 软件测试的生命周期软件测试&软件开发生命周期如何描述一个bug如何定义bug的级别bug的生命周期如何开始第一次测试测试的执行和BUG管理产生争执怎么办&#xff08;处理人际关系&#xff09; 大家好&#xff0c;我是晓星航。今天为大家带来的是 测试基础 相关的讲解…

C++最佳实践之编译篇

C最佳实践之工程编译 在大型c/c工程开发中&#xff0c;往往会涉及多级CMakeLists.txt的调用&#xff0c;并且调用方式错综复杂&#xff0c;主要有以下两种方式&#xff1a; 1. 子目录中的CMakeList.txt独立生成目标&#xff0c;不作为主目标生成过程的依赖关系&#xff08;比…

架构师技能9-深入mybatis:Creating a new SqlSession到查询语句耗时特别长

开篇语录&#xff1a;以架构师的能力标准去分析每个问题&#xff0c;过后由表及里分析问题的本质&#xff0c;复盘总结经验&#xff0c;并把总结内容记录下来。当你解决各种各样的问题&#xff0c;也就积累了丰富的解决问题的经验&#xff0c;解决问题的能力也将自然得到极大的…

【生活】浅浅记录

各位小伙伴们好鸭&#xff0c;今天不是技术文章&#xff0c;浅浅记录一下最近几个月的收获&#x1f60a; 新的一年&#xff0c;一起努力&#xff0c;加油加油&#xff01;