C++之深入解析C++20协程的原理和应用

news2025/1/17 3:14:34

一、无栈协程成为 C++20 协程标准

  • 协程分为无栈协程和有栈协程两种,无栈指可挂起/恢复的函数,有栈协程则相当于用户态线程。有栈协程切换的成本是用户态线程切换的成本,而无栈协程切换的成本则相当于函数调用的成本;无栈协程和线程的区别:无栈协程只能被线程调用,本身并不抢占内核调度,而线程则可抢占内核调度。
  • 协程函数与普通函数的区别:普通函数执行完返回,则结束,协程函数可以运行到一半,返回并保留上下文;下次唤醒时恢复上下文,可以接着执行。
  • 协程与多线程:
    • 协程适合 IO 密集型程序,一个线程可以调度执行成千上万的协程,IO 事件不会阻塞线程;
    • 多线程适合 CPU 密集型场景,每个线程都负责 cpu 计算,cpu 得到充分利用。
  • 协程与异步:
    • 都是不阻塞线程的编程方式,但是协程是用同步的方式编程、实现异步的目的,比较适合代码编写、阅读和理解;
    • 异步编程通常使用 callback 函数实现,将一个功能拆分到不同的函数,相比协程编写和理解的成本更高。

二、C++20 为什么选择无栈协程?

  • 有栈(stackful)协程通常的实现手段是在堆上提前分配一块较大的内存空间(比如 64K),也就是协程所谓的“栈”,参数、return address 等都可以存放在这个“栈”空间上。如果需要协程切换,那么通过 swapcontext 一类的形式来让系统认为这个堆上空间就是普通的栈,这就实现了上下文的切换。
  • 有栈协程最大的优势就是侵入性小,使用起来非常简便,已有的业务代码几乎不需要做什么修改,但是 C++20 最终还是选择了使用无栈协程,主要出于下面这几个方面的考虑。
  • 栈空间的限制:有栈协程的“栈”空间普遍是比较小的,在使用中有栈溢出的风险;而如果让“栈”空间变得很大,对内存空间又是很大的浪费。无栈协程则没有这些限制,既没有溢出的风险,也无需担心内存利用率的问题。
  • 性能:有栈协程在切换时确实比系统线程要轻量,但是和无栈协程相比仍然是偏重的,这一点虽然在目前的实际使用中影响没有那么大(异步系统的使用通常伴随了 IO,相比于切换开销多了几个数量级),但也决定了无栈协程可以用在一些更有意思的场景上。
  • 关于协程的储存空间:
    • C++ 的设计是无栈协程, 所有的局部状态都储存在堆上;
    • 储存协程的状态需要分配空间,分配 frame 的时候会先搜索 promise_type 有没有提供 operator new, 其次是搜索全局范围;
    • 有分配就可能会有失败,如果写了 get_return_object_on_allocation_failure() 函数,那就是失败后的办法,代替 get_return_object() 来完成工作(需要 noexcept);
    • 协程结束以后的释放空间也会先在 promise_type 里面搜索 operator delete,其次搜索全局范围;
    • 协程的储存空间只有在运行完 final_suspend 之后才会析构,或者得显式调用 coro.destroy(),否则协程的存储空间就永远不会释放,如果在 final_suspend 那里停下,那么就得在包装函数里面手动调用 coro.destroy(),不然就会漏内存;
    • 如果已经运行完毕 final_suspend,或者已经被 coro.destroy() 给析构了,那么协程的储存空间已经被释放,再次对 coro 做任何的操作都会导致 seg fault。

三、无栈协程是普通函数的泛化

  • 无栈协程是一个可以暂停和恢复的函数,是函数调用的泛化,这是为什么呢?我们知道一个函数的函数体(function body)是顺序执行的,执行完之后将结果返回给调用者,没办法挂起它并稍后恢复它,只能等待它结束。
  • 而无栈协程则允许我们把函数挂起,然后在任意需要的时刻去恢复并执行函数体,相比普通函数,协程的函数体可以挂起并在任意时刻恢复执行,因此,从这个角度来说,无栈协程是普通函数的泛化。

在这里插入图片描述

四、C++20 协程的“微言大义”

  • C++20 提供了三个新关键字(co_await、co_yield 和 co_return),如果一个函数中存在这三个关键字之一,那么它就是一个协程。
  • 编译器会为协程生成许多代码以实现协程语义,会生成什么样的代码?怎么实现协程的语义?协程的创建是怎样的?co_await 机制是怎样的?

① 协程帧(coroutine frame)

  • 当 caller 调用一个协程的时候会先创建一个协程帧,协程帧会构建 promise 对象,再通过 promise 对象产生 return object。
  • 协程帧中主要有这些内容:
    • 协程参数;
    • 局部变量;
    • promise 对象。
  • 这些内容在协程恢复运行的时候需要用到,caller 通过协程帧的句柄 std::coroutine_handle 来访问协程帧。

② promise_type

  • promise_type 是 promise 对象的类型,promise_type 用于定义一类协程的行为,包括协程创建方式、协程初始化完成和结束时的行为、发生异常时的行为、如何生成 awaiter 的行为以及 co_return 的行为等。
  • promise 对象可以用于记录/存储一个协程实例的状态,每个协程桢与每个 promise 对象以及每个协程实例是一一对应的。

③ coroutine return object

  • 它是 promise.get_return_object() 方法创建的,一种常见的实现手法会将 coroutine_handle 存储到 coroutine object 内,使得该 return object 获得访问协程的能力。

④ std::coroutine_handle

  • 协程帧的句柄,主要用于访问底层的协程帧、恢复协程和释放协程帧。
  • 可通过调用 std::coroutine_handle::resume() 唤醒协程。

⑤ co_await、awaiter、awaitable

  • co_await:一元操作符;
  • awaitable:支持 co_await 操作符的类型;
  • awaiter:定义了 await_ready、await_suspend 和 await_resume 方法的类型。
  • co_await expr 通常用于表示等待一个任务(可能是 lazy 的,也可能不是)完成。co_await expr 时,expr 的类型需要是一个 awaitable,而该 co_await表达式的具体语义取决于根据该 awaitable 生成的 awaiter。
  • 看起来和协程相关的对象还不少,这正是协程复杂又灵活的地方,可以借助这些对象来实现对协程的完全控制,实现任何想法。但是,需要先要了解这些对象是如何协作的,掌握了协程的原理,写协程应用也会游刃有余。

⑥ 协程对象的协作

  • 如下所示:
Return_t foo () { 
    auto res = co_await awaiter; 
    co_return res ; 
}
  • Return_t:promise return object,awaiter: 等待一个 task 完成:

在这里插入图片描述

  • 图中浅蓝色部分的方法就是 Return_t 关联的 promise 对象的函数,浅红色部分就是 co_await 等待的 awaiter。这个流程的驱动是由编译器根据协程函数生成的代码驱动的,分成三部分:
    • 协程创建;
    • co_await awaiter 等待 task 完成;
    • 获取协程返回值和释放协程帧。
  • 协程的创建:
Return_t foo () { 
    auto res = co_await awaiter; 
    co_return res ; 
}
  • foo() 协程会生成下面这样的模板代码(伪代码),协程的创建都会产生类似的代码:
{
  co_await promise.initial_suspend();
  try {
    coroutine body;
  }
  catch (...) {
    promise.unhandled_exception();
  }
FinalSuspend:
  co_await promise.final_suspend();
}
  • 首先需要创建协程,创建协程之后是否挂起则由调用者设置 initial_suspend 的返回类型来确定。
  • 创建协程的流程大概如下:
    • 创建一个协程帧(coroutine frame);
    • 在协程帧里构建 promise 对象;
    • 把协程的参数拷贝到协程帧里;
    • 调用 promise.get_return_object() 返回给 caller 一个对象,即代码中的 Return_t 对象。
  • 在这个模板框架里有一些可定制点:如 initial_suspend、final_suspend、unhandled_exception 和 return_value。可以通过 promise 的 initial_suspend 和 final_suspend 返回类型来控制协程是否挂起,在 unhandled_exception 里处理异常,在 return_value 里保存协程返回值。可以根据需要定制 initial_suspend 和 final_suspend 的返回对象来决定是否需要挂起协程,如果挂起协程,代码的控制权就会返回到 caller,否则继续执行协程函数体(function body)。

在这里插入图片描述

  • 值得注意的是,如果禁用异常,那么生成的代码里就不会有 try-catch,此时协程的运行效率几乎等同非协程版的普通函数,这在嵌入式场景很重要,也是协程的设计目的之一。

五、C++20 协程示例

  • 如下所示,通过 co_await 把协程调度到一个线程中打印一下线程 id:
#include <coroutine>
#include <iostream>
#include <thread>

namespace Coroutine {
  struct task {
    struct promise_type {
      promise_type() {
        std::cout << "1.create promie object\n";
      }
      task get_return_object() {
        std::cout << "2.create coroutine return object, and the coroutine is created now\n";
        return {std::coroutine_handle<task::promise_type>::from_promise(*this)};
      }
      std::suspend_never initial_suspend() {
        std::cout << "3.do you want to susupend the current coroutine?\n";
        std::cout << "4.don't suspend because return std::suspend_never, so continue to execute coroutine body\n";
        return {};
      }
      std::suspend_never final_suspend() noexcept {
        std::cout << "13.coroutine body finished, do you want to susupend the current coroutine?\n";
        std::cout << "14.don't suspend because return std::suspend_never, and the continue will be automatically destroyed, bye\n";
        return {};
      }
      void return_void() {
        std::cout << "12.coroutine don't return value, so return_void is called\n";
      }
      void unhandled_exception() {}
    };

    std::coroutine_handle<task::promise_type> handle_;
  };

  struct awaiter {
    bool await_ready() {
      std::cout << "6.do you want to suspend current coroutine?\n";
      std::cout << "7.yes, suspend becase awaiter.await_ready() return false\n";
      return false;
    }
    void await_suspend(
      std::coroutine_handle<task::promise_type> handle) {
      std::cout << "8.execute awaiter.await_suspend()\n";
      std::thread([handle]() mutable { handle(); }).detach();
      std::cout << "9.a new thread lauched, and will return back to caller\n";
    }
    void await_resume() {}
  };

  task test() {
    std::cout << "5.begin to execute coroutine body, the thread id=" << std::this_thread::get_id() << "\n";//#1
    co_await awaiter{};
    std::cout << "11.coroutine resumed, continue execcute coroutine body now, the thread id=" << std::this_thread::get_id() << "\n";//#3
  }
}// namespace Coroutine

int main() {
  Coroutine::test();
  std::cout << "10.come back to caller becuase of co_await awaiter\n";
  std::this_thread::sleep_for(std::chrono::seconds(1));

  return 0;
}
  • 测试输出:
1.create promie object
2.create coroutine return object, and the coroutine is created now
3.do you want to susupend the current coroutine?
4.don't suspend because return std::suspend_never, so continue to execute coroutine body
5.begin to execute coroutine body, the thread id=0x10e1c1dc0
6.do you want to suspend current coroutine?
7.yes, suspend becase awaiter.await_ready() return false
8.execute awaiter.await_suspend()
9.a new thread lauched, and will return back to caller
10.come back to caller becuase of co_await awaiter
11.coroutine resumed, continue execcute coroutine body now, the thread id=0x700001dc7000
12.coroutine don't return value, so return_void is called
13.coroutine body finished, do you want to susupend the current coroutine?
14.don't suspend because return std::suspend_never, and the continue will be automatically destroyed, bye
  • 可以清晰的看到协程是如何创建的、co_await 等待线程结束、线程结束后协程返回值以及协程销毁的整个过程。输出内容中的 1、2、3 展示了协程创建过程,先创建 promise,再通过 promise.get_return_object() 返回 task,这时协程就创建完成。协程创建完成之后是要立即执行协程函数呢?还是先挂起来?这个行为由 promise.initial_suspend() 来确定,由于它返回的是一个 std::suspend_never 的awaiter,因此不会挂起协程,于是就立即执行协程函数。
  • 执行协程到函数的 co_await awaiter 时,是否需要等待某个任务?返回 false 表明希望等待,于是接着进入到 awaiter.wait_suspend(),并挂起协程,在 await_suspend 中创建了一个线程去执行任务,之后就返回到 caller,caller 这时候可以不用阻塞等待线程结束,可以做其它事情,需要注意的是 awaiter 同时也是一个 awaitable,因为它支持 co_await。
  • 当线程开始运行的时候恢复挂起的协程,这时候代码执行会回到协程函数继续执行,这就是最终的目标:在一个新线程中去执行协程函数的打印语句。awaiter.final_suspend 决定是否要自动销毁协程,返回 std::suspend_never 就自动销毁协程,否则需要用户手动去销毁。

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

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

相关文章

个人写校园点评项目的笔记

目录 ​编辑 1.解决短信登陆--2023.4.14 redis 数据类型 阿里云短信服务 存入redis的key和value 流程 dto的意义 给token设置有效期 拦截器的类没有交给Spring Constants 2.商户查询缓存&#xff08;不采用SpringCache&#xff0c;而是尝试原理实现&#xff09; 20…

Spring Cloud Alibab --Seata

事务特性 A&#xff08;Atomic&#xff09;&#xff1a;原子性&#xff0c;构成事务的所有操作&#xff0c;要么都执行完成&#xff0c;要么全部不执行&#xff0c;不可能出现部分成功部分失败的情况。 C&#xff08;Consistency&#xff09;&#xff1a;一致性&#xff0c;在事…

Tarjan算法求割点和桥

先进行一些定义&#xff0c;假设目前有一个无向连通图 割点&#xff1a;某点及其边去掉后&#xff0c;图不再连通 桥&#xff1a;某条边去掉后&#xff0c;图不再联通 tarjan算法求割点 不考虑子结点到父结点的情况 dfn(x) x实际杯访问的时间点 low(x) x通过图可回溯到的最…

22从零开始学Java之你知道return、break与continue的区别吗?

作者&#xff1a;孙玉昌&#xff0c;昵称【一一哥】&#xff0c;另外【壹壹哥】也是我哦 千锋教育高级教研员、CSDN博客专家、万粉博主、阿里云专家博主、掘金优质作者 前言 在上一篇文章中&#xff0c;壹哥给大家介绍了while、do-while两种循环结构&#xff0c;并且给大家总结…

KubeSphere 社区双周报 | OpenFunction 支持 Dapr 状态管理 | 2023.03.31-04.13

KubeSphere 社区双周报主要整理展示新增的贡献者名单和证书、新增的讲师证书以及两周内提交过 commit 的贡献者&#xff0c;并对近期重要的 PR 进行解析&#xff0c;同时还包含了线上/线下活动和布道推广等一系列社区动态。 本次双周报涵盖时间为&#xff1a;2023.03.31-2023.…

测试工具之JMH详解

文章目录 1 JMH1.1 引言1.2 简介1.3 DEMO演示1.3.1 测试项目构建1.3.2 编写性能测试1.3.3 执行测试1.3.4 报告结果 1.4 注解介绍1.4.1 BenchmarkMode1.4.2 Warmup1.4.3 Measurement1.4.4 Threads1.4.5 Fork1.4.6 OutputTimeUnit1.4.7 Benchmark1.4.8 Param1.4.9 Setup1.4.10 Te…

大数据实战 --- 美团外卖平台

目录 开发环境 数据描述 功能需求 数据准备 数据分析 RDD操作 Spark SQL操作 创建Hbase数据表 创建外部表 统计查询 开发环境 HadoopHiveSparkHBase 启动Hadoop&#xff1a;start-all.sh 启动zookeeper&#xff1a;zkServer.sh start 启动Hive&#xff1a; nohup …

Netty中的HttpServerCodec和HttpObjectAggregator

首先使用Netty搭建一个HttpServer&#xff0c;代码如下&#xff1a; public class App {public static boolean useEpoll false;static {String os System.getProperty("os.name");if (Objects.nonNull(os) && os.equalsIgnoreCase("linux") &a…

Git分支篇git branch和git checkout

分支作用 在开发过程中&#xff0c;项目往往由多人协同开发&#xff0c;那么将多人编写的代码汇总到一起就成了一个困难且复杂的工作&#xff0c;另外项目也需要备份和版本迭代&#xff0c;因此不能只有一个版本。因此分支就成为了优秀的解决方案。 分支相互独立&#xff0c;…

C++STL详解(九)--使用红黑树封装实现set和map

文章目录 控制底层红黑树模板参数模板参数中的仿函数map,set中的正向迭代器map,set中的反向迭代器[]下标访问运算符重载map的模拟实现代码map的模拟实现适用map,set容器的底层红黑树代码(修改版本) 控制底层红黑树模板参数 如果我们用一棵KV模型的红黑树同时实现map和set,我们…

【数据结构】八大排序算法(梭哈)

目录 1.直接插入排序2. * 希尔排序关于希尔排序的时间复杂度 3.选择排序4. * 堆排序5.冒泡排序6. * 快速排序6.1递归快排6.1.1 hoare版6.1.2 挖坑法6.1.3 前后指针法6.1.4 关于每个区间操作的结束位置总是小于key6.1.5 关于有序原数据的效率优化 6.2 非递归快排 7. * 归并排序7…

计算机网络考试复习——第五章

本章考察范围为5.1 5.3 5.4这三部分。 该层传输的单位是报文段 5.1 运输层协议概述&#xff1a; 5.1.1 进程之间的通信&#xff1a; 运输层是向它上面的应用层提供通信服务。它属于面向通信部分的最高层&#xff0c;同时也是用户功能的最低层。 屏蔽作用&#xff1a;运输层…

flutter系列之:如何自定义动画路由

文章目录 简介自定义跳转使用flutter动画基础实现一个自定义的route总结 简介 flutter中有默认的Route组件&#xff0c;叫做MaterialPageRoute&#xff0c;一般情况下我们在flutter中进行跳转的话&#xff0c;只需要向Navigator中传入一个MaterialPageRoute就可以了。 但是Ma…

让你的three.js动起来

让你的three.js动起来 简介 本节主要是给实例添加动画效果&#xff0c;以及加了一些小插件用以实现帧率检测、gui可视化配置、动态监听屏幕大小变化刷新和鼠标操控功能。 引入的插件js&#xff1a; three.jsdat.gui.jsStats.jsTrackballControls.js 实际效果&#xff1a; …

Java 线程状态有哪些?

文章目录 Java 线程状态有哪些&#xff1f;初始状态&#xff08;NEW&#xff09;可运行状态&#xff08;RUNNABLE&#xff09;就绪状态运行状态 阻塞状态&#xff08;BLOCKED&#xff09;等待状态&#xff08;WAITING&#xff09;超时等待&#xff08;TIMED_WAITING&#xff09…

一次打靶场记录

题目提示 1、目录爆破 在对靶场进行信息收集、目录扫描之后发现结果存在www.zip,data.zp 两个备份文件 下载回来解压之后发现www.zip是网站备份文件&#xff0c;data.zip是数据库文件&#xff0c;存在一个maccms的数据库 苹果cms的数据库&#xff0c;导入本地数据库。 admin…

【并发编程Python】为什么Python这么慢

Python速度慢的所有原因 Python作为一门非常优秀的计算机语言&#xff0c;其速度慢给Python减了很多分&#xff0c;也是其一直被诟病的主要原因之一&#xff0c;通常情况下&#xff0c;Python比Java/C慢约5-10倍&#xff0c;在一些特殊的情况下Python甚至比C慢100~200倍&#x…

数据结构——B树和B+树

数据结构——B树和B树 一、B树1.B树的特征2.B树的插入操作3.B树的删除操作4.B树的缺点 二、B树B树的特征 平衡二叉树或红黑树的查找效率最高&#xff0c;时间复杂度是O(nlogn)。但不适合用来做数据库的索引树。 因为磁盘和内存读写速度有明显的差距&#xff0c;磁盘中存储的数…

玩转肺癌目标检测数据集Lung-PET-CT-Dx ——③整理、验证数据,建立Dataset对象

文章目录 数据整理整理出所有的dcm文件整理出所有的xml标注文件整理数据①——舍弃错误的标注文件整理数据②——两个标注文件指向同一个目标图片的情况封装函数&#xff0c;传入xml文件&#xff0c;显示标注效果 整理数据③——将PETCT的三通道图像转成平扫CT的单通道图像格式…

企业费控,驶向「一体化」

在数字化于企业内部各个环节实现平权、成为标配的当下&#xff0c;财务&#xff0c;这个被称为“控制企业生命力”的核心环节&#xff0c;是否应该拥有新的价值&#xff1f; 作者| 皮爷 出品|产业家 2022年年中&#xff0c;施伟和分贝通的团队接到一项“特殊需求”。 这个…