C++-一篇文章入门coroutines协程

文章目录

  • 前言
  • 什么是协程
  • 协程实现原理
  • C++协程的最小例子
      • 1
      • 2
      • 3
      • 4
      • 5
      • 协程等效代码
  • 协程传值的例子

前言

最近学习了一下C++协程,这篇文章将介绍协程的相关概念,以及在C++中如何使用协程。

什么是协程

C++中,协程(coroutines)可以理解为一个可以暂停和恢复执行的函数。什么意思呢?例如有以下协程函数:

Task taskFunc()
{...co_await doSomething();  // 1doSomething2();			 // 2
}int main() {auto task = taskFunc(); // 3...						// 4task.resume();			// 5
}

3处调用协程函数,在执行到1的时候(例如doSomething()异步请求网络资源),函数暂停执行,代码走到4处,等后面某个合适的时机5,恢复执行协程函数,函数从2继续执行,对数据进行处理,执行顺序是3->1->4->5->2,在这个过程中,执行的线程始终是同一个(除非有意让协程在不同线程中切换执行)。

协程实现原理

C++中,协程是通过编译器来实现的,编译器根据代码中的co_return/co_await关键字,识别一个函数为协程函数,协程函数的返回值类型有如下要求:返回值是一个类,类中包含一个名称为promise_type的类型(必须是这个类名),promise_type必须实现几个函数:

  • get_return_object
  • initial_suspend
  • final_suspend
  • unhandled_exception
  • return_void或者return_value
    通常来说,这个外层的类会有一个std::coroutine_handle\<promise_type\> handle成员保存协程的句柄,用于控制协程和获取协程的相关数据。编译器会将协程状态和协程中的数据保存在堆上,并生成执行协程所需的代码。

C++协程的最小例子

#include <iostream>
#include <coroutine>template <bool READY>
struct Awaiter {bool await_ready() noexcept {std::cout << "await_ready: " << READY << std::endl;return READY;}void await_resume() noexcept {std::cout << "await_resume" << std::endl;}void await_suspend(std::coroutine_handle<>) noexcept {std::cout << "await_suspend" << std::endl;}
};struct TaskPromise {struct promise_type {TaskPromise get_return_object() {std::cout << "get_return_object" << std::endl;return TaskPromise{std::coroutine_handle<promise_type>::from_promise(*this)};}Awaiter<true> initial_suspend() noexcept {std::cout << "initial_suspend" << std::endl;return {};}Awaiter<true> final_suspend() noexcept {std::cout << "final_suspend" << std::endl;return {};}void unhandled_exception() {std::cout << "unhandled_exception" << std::endl;}void return_void() noexcept {std::cout << "return_void" << std::endl;}};void resume() {std::cout << "resume" << std::endl;handle.resume();}std::coroutine_handle<promise_type> handle;
};TaskPromise task_func() {std::cout << "task first run" << std::endl;co_await Awaiter<false>{};std::cout << "task resume" << std::endl;
}int main() {auto promise = task_func();  // 1promise.resume();return 0;
}

接下来我们一步一步解析协程的执行过程:

1

首先task_func就是如前所述的协程函数,因为它包含了一个co_await关键字,返回值是TaskPromise类型,其包含了一个promist_type类型,实现了一些必须的函数,满足协程的所有要求。

当一个协程被调用时,编译器不会像普通函数一样在栈上分配空间。相反,它会在堆上动态分配一块内存,这块内存被称为“协程帧”。这个帧里包含了:

  • 协程的 Promise 对象 (promise_type)
  • 所有按值传递的参数的拷贝
  • 所有在 co_await 挂起点之间需要保持状态的局部变量
  • 协程当前执行到的位置(状态机的状态)

总结来说就是,第一次执行协程函数task_func时,编译器会在堆上创建一个协程帧用来保存协程状态和局部变量等信息。接着,调用promise_type的get_return_object方法,该函数的返回值必须和协程函数的返回值一致,在task_func暂停/结束执行时,返回值就是get_return_object的返回值。

get_return_object函数的实现通常是返回外层类对象,并使用promise_type对象初始化coroutine_handle:

std::coroutine_handle<promise_type>::from_promise(*this)

std::coroutine_handle可以理解为类似std::thread的类,是一个wrapper,用来控制协程和获取协程数据的。from_promise将一个promise_type类型转换为std::coroutine_handle,相应的std::coroutine_handle::promise方法用于获取handle中的promise_type对象。

2

接着,执行的是initial_suspend,这个函数的返回值是一个类类型,通常称为awaiter,这个类用于控制接下来要继续执行协程函数,还是暂停执行,这个类必须实现如下函数:await_ready,await_suspend,await_resume,关于这三个函数的详细解释,我们等到co_await时再说。
initial_suspend返回类型可以使用stl提供的两种awaiter:std::suspend_always和std::suspend_never,分别表示总是挂起或者总是继续执行。其实现也很简单:

// STRUCT suspend_always
struct suspend_always {_NODISCARD constexpr bool await_ready() const noexcept {return false;}constexpr void await_suspend(coroutine_handle<>) const noexcept {}constexpr void await_resume() const noexcept {}
};

这样就不用自己实现awaiter了,在我们的例子中,我们实现了自己的awaiter类,并且await_ready返回true,此时会立即调用await_resume。

3

接着开始真正执行task_func函数的第一行

std::cout << "task first run" << std::endl;

然后我们遇到了co_await表达式,co_await后面跟着的是awaiter对象,我们详细的介绍一下awaiter的三个函数:

  • await_ready。没有参数,返回bool类型,表示接下来应该继续执行(true)还是暂停执行(false)
  • await_suspend。当await_ready返回false时,表示没有准备好数据,此时下一个执行的函数就是await_suspend。await_suspend参数是协程句柄类型std::coroutine_handle,这个参数是编译器帮忙传入的。await_suspend返回值可以是void,也可以是bool(如果返回false则又会继续执行协程),甚至可以是其他协程句柄,从而执行其他协程(这是高级话题,我们以后再说)
  • await_resume。当await_ready返回true时,表示已经准备好数据,此时下一个执行的函数就是await_resume。await_resume没有参数,返回值可以是任意类型,这个返回值会作为co_return表达式的返回值。

在我们的例子中,co_await Awaiter<false>{};会让协程挂起,于是编译器将当前表达式生成的awaiter对象保存在协程帧中,然后协程函数task_func返回一个TaskPromise对象(由前面所说的get_return_object构造),回到了main中执行。

4

接着我们调用了TaskPromise::resume,这个函数只做了一件事,就是调用coroutine_handle的resume方法,它会调用之前co_wait挂起时保存的awaiter对象的resume方法,然后继续执行协程函数:

std::cout << "task resume" << std::endl;

5

最后,协程函数结束执行,我们没有写co_return,编译器会默认补上co_return在最后,co_return会调用promise_type::return_void()函数,表示没有返回值。如果有返回值,就需要定义一个叫void return_value(T t)的函数,return_value和return_void不能共存。
接着编译器调用promise_type::final_suspend结束协程,final_suspend类似initial_suspend,如果挂起,协程不会立即销毁内部的状态信息,反之则会立即销毁,因为我们可能还有部分信息存在promise_type对象中,所以在finial_suspend挂起后,则需要手动释放coroutine_handle的资源,可以采用RAII的方式,在外层类中的析构函数释放coroutine_handle:

~TaskPromise()
{handle.destroy();
}

如果发生异常,则会调用promise::unhandled_exception。

协程等效代码

综上,我们可以写出task_func协程执行过程的伪代码:

TaskPromise task_func() {// No parameters and local variables.auto state = new __TaskPromise_state_(); // has TaskPromise::promise_type promise; TaskPromise coro = state.promise.get_return_object();try {co_await p.inital_suspend();std::cout << "task first run" << std::endl;co_await Awaiter<false>{};std::cout << "task resume" << std::endl;} catch (...) {state.promise.unhandled_exception();}co_await state.promise.final_suspend();
}

协程传值的例子

下面是一个模拟通过协程获取数据,最终返回在main中取数据的例子


#include <iostream>
#include <coroutine>
#include <future>
#include <thread>struct TaskPromise {struct promise_type {TaskPromise get_return_object() {std::cout << "get_return_object(), thread_id: " << std::this_thread::get_id() << std::endl;return TaskPromise{std::coroutine_handle<promise_type>::from_promise(*this)};}std::suspend_always initial_suspend() noexcept { return {}; }std::suspend_always final_suspend() noexcept { return {}; }void unhandled_exception() {}void return_void() noexcept {}size_t data = 0;};std::coroutine_handle<promise_type> handle;
};struct Awaiter {bool await_ready() noexcept {std::cout << "await_ready(), thread_id: " << std::this_thread::get_id() << std::endl;return false;}void await_suspend(std::coroutine_handle<TaskPromise::promise_type> handle) noexcept {std::cout << "await_suspend(), thread_id: " << std::this_thread::get_id() << std::endl;auto thread = std::thread([=]() {std::this_thread::sleep_for(std::chrono::seconds(1));handle.promise().data = 1;handle.resume();});thread.join();}void await_resume() noexcept {std::cout << "await_resume(), thread_id: " << std::this_thread::get_id() << std::endl;}
};TaskPromise task_func() {std::cout << "task_func() step 1, thread_id: " << std::this_thread::get_id() << std::endl;co_await Awaiter{};std::cout << "task_func() step 2, thread_id: " << std::this_thread::get_id() << std::endl;
}int main() {std::cout << "main(), thread_id: " << std::this_thread::get_id() << std::endl;auto promise = task_func();std::cout << "main(), data: " << promise.handle.promise().data << ", thread_id: " << std::this_thread::get_id() << std::endl;promise.handle.resume();std::cout << "main(), data: " << promise.handle.promise().data << ", thread_id: " << std::this_thread::get_id() << std::endl;return 0;
}

参考:1. https://mp.weixin.qq.com/s/0njDHtz_SGPkrr4ndAWHaA

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

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

相关文章

数字经济专业的就业全景指南

CDA数据分析师证书含金量高&#xff0c;适应了未来数字化经济和AI发展趋势&#xff0c;难度不高&#xff0c;行业认可度高&#xff0c;对于找工作很有帮助。一、数字经济就业热力图二、核心岗位发展路径1. 互联网数字运营岗2. 金融科技岗岗位类型技能组合证书加持5年薪资范围智…

PDF转Word免费工具!批量处理PDF压缩,合并, OCR识别, 去水印, 签名等全功能详解

大家好&#xff0c;欢迎来到程序视点&#xff01;我是你们的老朋友.小二&#xff01;前言PDF软件我发的非常多&#xff0c;但今天这款工具是大家公认最值得推荐的&#xff0c;这款软件就是PDF24PDF24几乎包含了PDF的所有功能&#xff0c;目前是更新到了最新版本&#xff01;文末…

Flutter开发实战之Widget体系与布局原理

第3章:Widget体系与布局原理 在前面两章中,我们已经搭建好了Flutter开发环境,并且了解了Dart语言的基础知识。现在是时候深入Flutter的核心——Widget体系了。如果说Dart是Flutter的语言基础,那么Widget就是Flutter的灵魂。理解Widget体系,是掌握Flutter开发的关键所在。…

C++:stack与queue的使用

stack与queue的使用一.stack与queuej基础1.stack1.1基本认识1.2示例代码代码功能解析2.queue2.1基础知识操作说明2.2示例代码代码分析 一.stack与queuej基础 1.stack 1.1基本认识以上图片展示了栈&#xff08;stack&#xff09;这种数据结构的基本操作示意。栈是一种遵循后进先…

Unity 编辑器开发 之 Excel导表工具

一个简单的Excel导表工具&#xff0c;可以用来热更数据配置工具使用&#xff1a;&#xfeff;&#xfeff;执行菜单 SDGSupporter/Excel/1.Excel2Cs 生成c#脚本。&#xfeff;&#xfeff;等待C#类编译完成&#xfeff;&#xfeff;执行菜单 SDGSupporter/Excel/2.Excel2Bytes …

【数据结构与算法】力扣 415. 字符串相加

题目描述 415. 字符串相加 给定两个字符串形式的非负整数 num1 和num2 &#xff0c;计算它们的和并同样以字符串形式返回。 你不能使用任何內建的用于处理大整数的库&#xff08;比如 BigInteger&#xff09;&#xff0c; 也不能直接将输入的字符串转换为整数形式。 示例 1…

进阶向:Manus AI与多语言手写识别

Manus AI与多语言手写识别:从零开始理解 手写识别技术作为人工智能领域的重要应用之一,近年来在智能设备、教育、金融等行业得到了广泛运用。根据市场调研机构IDC的数据显示,2022年全球手写识别市场规模已达到45亿美元,预计到2025年将突破70亿美元。其中,多语言手写识别技…

Javaweb————HTTP请求头属性讲解

❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️前面我们已经说过http请求分为三部分&#xff0c;请求行&#xff0c;请求头和请求体 请求头包含若干个属性&#xff1a;格式为属性名&#xff1a;属性值&#xff0c;这篇文章我们就来介绍一下http请求头中一些常见属性的含义 我们…

9.c语言常用算法

查找顺序查找&#xff08;线性查找&#xff09;算法思想&#xff1a;从数组的第一个元素开始&#xff0c;逐个与目标值进行比较&#xff0c;直到找到目标值或查找完整个数组。时间复杂度&#xff1a;最好情况&#xff1a;O(1)&#xff08;目标在第一个位置&#xff09;最坏情况…

AI小智源码分析——音频部分(一)

一、源码跳转这里采用了函数重载来进行代码复用&#xff0c;当需要对I2S接口的数据进行配置&#xff0c;比如左右音道切换&#xff0c;可以使用第二个构造函数&#xff0c;这里小智使用的是第一个构造函数&#xff0c;即只传递I2S相关的引脚参数&#xff08;不带slot mask&…

【GNSS原理】【LAMBDA】Chapter.12 GNSS定位算法——模糊度固定LAMBDA算法[2025年7月]

Chapter.12 GNSS定位算法——模糊度固定LAMBDA算法 作者&#xff1a;齐花Guyc(CAUC) 文章目录Chapter.12 GNSS定位算法——模糊度固定LAMBDA算法一.整周模糊度理论1.LAMBDA算法干了一件什么事情&#xff1f;2.LAMBDA算法步骤&#xff08;1&#xff09;去相关&#xff08;Z变换…

计算机毕业设计java在线二手系统的设计与实现 基于Java的在线二手交易平台开发 Java技术驱动的二手物品管理系统

计算机毕业设计java在线二手系统的设计与实现z2n189&#xff08;配套有源码 程序 mysql数据库 论文&#xff09; 本套源码可以在文本联xi,先看具体系统功能演示视频领取&#xff0c;可分享源码参考。随着互联网技术的飞速发展&#xff0c;二手交易市场也逐渐从传统的线下模式转…

如何进行项目复盘?核心要点分析

进行项目复盘需要明确复盘目标、确定复盘参与人员、选择合适的复盘方法、梳理项目过程与关键节点、分析成功与失败的原因、总结经验教训并制定改进计划。其中&#xff0c;选择合适的复盘方法尤其关键&#xff0c;常见的复盘方法包括鱼骨图分析法、SWOT分析法、PDCA循环法&#…

LeetCode 923.多重三数之和

给定一个整数数组 arr &#xff0c;以及一个整数 target 作为目标值&#xff0c;返回满足 i < j < k 且 arr[i] arr[j] arr[k] target 的元组 i, j, k 的数量。 由于结果会非常大&#xff0c;请返回 109 7 的模。 示例 1&#xff1a; 输入&#xff1a;arr [1,1,2,2,…

.Net日志系统Logging-五

日志概念 日志级别 NET (Microsoft.Extensions.Logging) 中定义的 6 个标准日志级别&#xff0c;按严重性从低到高排列&#xff1a; 日志级别数值描述典型使用场景Trace0最详细的信息&#xff0c;包含敏感数据&#xff08;如请求体、密码哈希等&#xff09;。仅在开发或深度故…

中国贸促会融媒体中心出海活动负责人、出海星球创始人莅临绿算技术

近日&#xff0c;中国贸促会融媒体中心出海活动负责人、出海星球创始人王思诺一行莅临广东省绿算技术有限公司&#xff0c;深入考察其核心技术产品与全球化布局。双方围绕绿算技术全栈产品体系、创新出海模式及生态共建展开深度对话。绿算技术作为国内智算基础设施领域的领军企…

【算法专题训练】06、数组双指针

1、数组 数组是由相同类型的元素组成的数据集合&#xff0c;并且占据一块连续的内存&#xff0c;按照顺序存储数据。 1.1、数组的特性&#xff1a; 数组元素通过下标获取数据数组对象初始化时&#xff0c;需要先指定数组容量大小&#xff0c;并根据容量大小分配内存。缺点&…

操作系统-lecture2(操作系统结构)

回顾下lecture1 swap区域不可以马上执行&#xff0c;即虚拟内存的数据和指令不可以被执行&#xff0c;得交换回到内存区域 操作系统的服务 主要提供两种服务 面向普通用户&#xff1a;user interface面向程序员&#xff1a;应用级程序代码 为用户 为用户提供了操作包括但不…

内网服务器实现从公网穿透

从6月份tplink的ddns失效之后&#xff0c;对于部分在内网运行的服务器&#xff0c;从公网访问就收到了部分影响。有好几个朋友找来&#xff0c;寻求帮助&#xff0c;看看怎么恢复原来的机制&#xff0c;可以从公网互联网访问内网服务器。方案一&#xff1a;如果有动态公网的客户…

vcs-编译+仿真+dump波形【IMP】

VCS仿真分为两步式(编译/compilation仿真/simulation)和三步式(分析/analysis细化/elaborationsimulation/仿真);注2:analysis/分析是三步式flow中仿真design的第一步&#xff0c;在此阶段将使用vhdlan或vlogan分析VHDL、Verilog、SystemVerilog和OpenVera文件。下面的部分包括…