【C++学习(35)】在Linux中基于ucontext实现C++实现协程(Coroutine),基于C++20的co_await 协程的关键字实现协程

文章目录

  • 为什么使用协程
  • 协程的理解
  • 协程优势
  • 协程的原语操作
    • yield 与 resume 是一个switch操作(三种实现方式):
  • 基于 ucontext 的协程
    • 基于 XFiber 库的操作
      • 1 包装上下文
      • 2 XFiber 上下文调度器
        • 2.1 CreateFiber
        • 2.2 Dispatch
  • 基于C++20的co_return 协程的关键字实现协程
  • 参考

为什么使用协程

从性能方面来看,对于使用异步 io 的线程,存在三个问题:

  • 系统线程占用大量的内存空间
  • 线程切换占用大量的系统时间
  • 为了线程安全,线程间需要加锁保护资源,降低执行的效率

从编程角度来看,无论同步还是异步编程方式,都是基于事件驱动的。事件驱动流程包括注册事件,绑定回调,触发回调,提高了系统的并发。但是由于回调的多层嵌套,使得编程复杂,降低了代码的可维护性。

在资源有限的前提下,高性能服务需要解决的问题有:

  • 减少线程的重复高频创建:线程池
  • 尽量避免线程的阻塞
  • Reactor + 非阻塞回调:解决问题的能力有限
  • 响应式编程:容易陷入回调地狱,割裂业务逻辑
  • 协程:将同 io 转成异步 io
  • 提升代码的可维护与可理解性:减少回调函数,减少回调链深度

而协程的出现,可以很好地解决上述问题。

协程的理解

协程(Coroutine)是一种能够挂起个恢复的函数过程 是一种轻量级的并发编程方式,也称为用户级线程。它与传统的线程(Thread)相比,具有更低的开销和更高的执行效率。 协程通常运用在异步调用中。

协程运行在线程之上。当一个协程调用阻塞 io,主动让出 cpu ( yield 原语) ,让另一个协程运行在当前线程之上( resume 原语)。协程没有增加线程数量,只是在线程的基础上通过分时复用的方式运行多个协程,降低了系统内存。而且协程的切换在用户态完成,减少了系统切换开销。

协程优势

消耗系统资源和切换代价更小
协程可以实现无锁编程
简化了异步编程,可以达到以同步的编程方式实现异步的性能。

  • 协程适用于 I/O 密集型业务,线程切换频繁。其他情况,性能不会有太大的提升。

协程的原语操作

  • yield: 协程主动让出CPU给调度器。时机:业务提交 -> epoll_wait
  • resume: 调度器恢复协程的运行权。时机:epoll_wait -> 业务处理
  • resume 和 yield 是两个可逆的原子操作。

yield 与 resume 是一个switch操作(三种实现方式):

  • 1.longjump/setjump
  • 2.ucontext
  • 3.汇编实现

基于 ucontext 的协程

协程的实现与线程的主动切换有关,当“当前上下文”可能阻塞时,需要主动切换到其它上下文来避免操作系统将当前线程挂起从而降低效率。

在Linux中定义了ucontext_t结构体来表示线程的上下文结构。

typedef struct ucontext_t {
  struct ucontext_t *uc_link;//表示当当前上下文阻塞时会被切换的上下文。
  sigset_t           uc_sigmask;//被当前线程屏蔽的信号
  stack_t 					 uc_stack;//线程栈
  mcontext_t 				 uc_mcontext;//与机器相关的线程上下文的表示
} ucontext_t;

与上下文相关的有四个函数:

    getcontext(ucontext_t* ucp): 调用后基于当前上下文初始化ucp所指向的上下文结构体。
    setcontext(const ucontext_t* ucp): 切换到ucp所指向的上下文,如果调用成功则不会返回,因为上下文已经被切换。
    makecontext(ucontext_t* ucp, void (*func)(), int argc, ...): 用于指定上下文需要执行的函数,要求在调用之前context已经确定栈和 uc_link. 当切换到该上下文后,函数func就会被执行。函数返回后,后继线程就会被切换到,如果uc_link为NULL,则线程退出。
    swapcontext(ucontext_t* restrict oucp, const ucontext_t* restrict ucp): 将当前上下文保存到oucp中,然后切换到ucp对应的上下文中。与setcontext的区别在于是否保存当前上下文。

附上stack_t的定义:

typedef struct {
  void* ss_sp;
  int ss_flags;
  size_t ss_size;
} stack_t;

比如可以通过下面的程序实现循环打印:

#include <ucontext.h>
#include <unistd.h>
#include <stdio.h>

int main() {
    int idx = 0;
    ucontext_t ctx1;
    getcontext(&ctx1);

    printf("%d\n", idx);
    idx++;
    sleep(1);
    setcontext(&ctx1);
    return 0;
}

基于 XFiber 库的操作

https://github.com/HiYx/xfiber

以XFiber为例讲解一下一个轻量级协程库的基本实现方式。

1 包装上下文

Linux提供的协程结构体比较简陋,并不足以供协程库使用,因此需要进行包装一下。

struct Fiber {
  uint64_t seq_;
  XFiber* xfiber_;
  std::string fiber_name_;
  ucontext_t ctx_;
  uint8_t* stack_ptr_;
  size_t stack_size_;
  std::function<void()> run_;
  WaitingEvents waiting_events_;
};

其中XFiber为协程的调度器,后面会讲。WatingEvents为协程所需要等待的读和写的文件描述符。

struct WaitingEvents {
  std::vector<int> waiting_fds_w_;
  std::vector<int> waiting_fds_r_;
  int64_t expire_at_;
};

2 XFiber 上下文调度器

2.1 CreateFiber

先从最基本的创建一个协程开始,首先注意协程和线程的区别,协程代表一段可以分开执行的逻辑,但是和其它协程还是保持串行执行,因此协程创建并不会马上执行,而是由协程调度器统一执行。

先看看创建协程函数的签名:

void XFiber::CreateFiber(std::function<void()> run, size_t stack_size, std::string fiber_name);

run即要执行的函数,这里作者设定了只能是无参数、无返回值的函数类型,但是其实可以借助C++模板实现各种类型函数的注册。

协程调度器主要维护两个协程队列,分别是运行队列和就绪队列,运行队列中的协程会被切换到,而就绪队列中的协程会在下一次的循环中被切换到。

同时维护两个map,io_waiting_fibers_表示监听的文件描述符所对应的一对读和写的协程,expire_fibers_的value为一个有序集合,表示在某个时间点会超时的协程集合。

2.2 Dispatch

当Dispatch函数开始运行时,各协程才开始运行。该函数主要分为三个部分:

  • 处理已经就绪的协程。将就绪队列move到运行队列中,然后将就绪队列清空,这样做的原因是这一循环的就绪队列在运行中可能重新回到就绪队列中(主动Yield就会回到就绪队列)。

  • 协程切换过程就涉及到上面的swapcontext函数,为了使得协程能够在返回后能够重新回到XFiber中,其结构体中维护了一个sched_ctx_成员表示调度器的上下文。因此每一条Fiber在被创建时都将sched_ctx_作为接下来切换到的上下文,这样就保证了每一条协程在执行完成以后都能过回到调度器来,并由调度器处理接下来的就绪协程。

  • 检查超时的协程,对于超时的协程集合,需要将这些协程通过 WakeupFiber 函数进行唤醒。

  • 调用 epoll 相关方法,检查所有的epoll事件,并唤醒相关协程。

基于C++20的co_return 协程的关键字实现协程

co_return :co_return 是 C++20 中引入的关键字,用于在协程中返回结果或结束协程。它用于替代 return 关键字,在协程函数中表示返回值,并触发协程的完成。

main函数创建了一个进程, 进程里面创建了一个主线程,然后执行每个函数就是子线程。

  • 先进入bar()函数, 先执行call bar ,
  • 然后执行before bar 经过挂起点然后挂起,
  • 这时候一个线程跳出了bar函数, 到main里面,
  • 另一个线程执行fool函数,
  • fool函数执行完以后, 再回到bar 函数里面继续执行

这就是本代码的大概思路

在这里插入图片描述

参考

https://blog.csdn.net/txh1873749380/article/details/134174067
https://www.cnblogs.com/kaleidopink/p/16387004.html
https://blog.csdn.net/m0_74036006/article/details/135960299

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

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

相关文章

用jquery做一个websocket客户端

先看效果图&#xff1a; 功能很简单&#xff0c;就是作为客户端连接websocket&#xff0c;并实现接受和发送消息。具体代码如下&#xff1a; <!DOCTYPE html> <html lang"zh-cn"> <head><meta charset"UTF-8"><meta name"…

本地音乐服务器(二)

4. 上传音乐模块设计 4.1 上传音乐的接口设计 请求和响应设计&#xff1a; 新建music实体类&#xff1a; Data public class Music {private int id;private String title;private String singer;private String time;private String url;private int userid; } 4.2 创建Mu…

GRPC实现

1.首先下载对应编译插件&#xff0c;这里不再提供下载 2.编写proto文件 3.编写完成用命令生成go文件 protoc --go_out. --go-grpc_out. *.proto --go_out. 其中的. 是说你要编译的 .proto 文件目录为当前目录&#xff0c;按需修改 --go-grpc_out.&#xff0c;其中的. 是说你生…

「Java EE开发指南」如何使用Visual JSF编辑器设计JSP?(一)

Visual JSF Designer的目标是使创建JSF应用程序的特定于组件的工作更容易可视化&#xff0c;在本教程中&#xff0c;您将使用可视化设计器设计JSF登录页面&#xff0c;将学习如何&#xff1a; 创建一个JSF项目创建一个新的JSF页面设计JSF页面 该功能在MyEclipse中可用。 MyE…

大模型(LLMs)RAG 版面分析——表格识别方法篇

大模型&#xff08;LLMs&#xff09;RAG 版面分析——表格识别方法篇 一、为什么需要识别表格&#xff1f; 表格的尺寸、类型和样式展现出多样化的特征&#xff0c;如背景填充的差异性、行列合并方法的多样性以及内容文本类型的不一致性等。同时&#xff0c;现有的文档资料不…

力扣(leetcode)题目总结——动态规划篇

leetcode 经典题分类 链表数组字符串哈希表二分法双指针滑动窗口递归/回溯动态规划二叉树辅助栈 本系列专栏&#xff1a;点击进入 leetcode题目分类 关注走一波 前言&#xff1a;本系列文章初衷是为了按类别整理出力扣&#xff08;leetcode&#xff09;最经典题目&#xff0c…

WebSocket实战,后台修改订单状态,前台实现数据变更,提供前端和后端多种语言

案例场景&#xff1a; 在实际的后台中需要变更某个订单的状态&#xff0c;在官网中不刷新页面&#xff0c;可以自动更新状态 在前端页面实现订单状态的实时更新&#xff08;不刷新页面&#xff09;&#xff0c;可以通过 WebSocket 的方式与后台保持通信&#xff0c;监听订单状态…

【Java 学习】数据类型、变量、运算符、条件控制语句

Java基础语法 1. 打印 Hello World !2. 变量类和数据类型2.1 什么是变量&#xff1f;什么是数据类型&#xff1f;2.2 常用的数据类型2.3 使用变量2.4 String 类数据类型2.4.1 String 类基本概念2.4.2 String 类的使用 3. 运算符3.1 算数运算符3.2 关系运算符3.3 逻辑运算符3.4 …

面试题:Kafka(一)

1. Kafka如何保证消息不丢失 生产者发送消息到Brocker丢失 设置异步发送 消息重试 消息在Brocker中存储丢失 发送确认机制acks 消费者从Brocker接收消息丢失 Kafka 中的分区机制指的是将每个主题划分成多个分区&#xff08;Partition&#xff09;topic分区中消息只能由消费者…

[Redis#1] 前言 | 再谈服务端高并发分布式结构的演进

目录 电子商务应用架构演进 概述 常见概念 架构演进 总结 总结 应用&#xff08;Application&#xff09;/ 系统&#xff08;System&#xff09; 模块&#xff08;Module&#xff09;/ 组件&#xff08;Component&#xff09; 分布式&#xff08;Distributed&#xff0…

洛谷刷题日记||基础篇9(线性表)

代码思路&#xff1a; 初始化圈&#xff1a;利用 std::list 保存编号为 1 到 n 的人。循环报数&#xff1a;利用迭代器模拟报数的过程&#xff0c;每次数到 m 时将对应的人出圈。循环处理&#xff1a;std::list::erase 删除出圈的人&#xff0c;并返回下一个人的迭代器&#x…

Elasticsearch开启认证及kibana密码登陆

Elasticsearch不允许root用户运行,使用root用户为其创建一个用户es,为用户es配置密码,并切换到es用户。 adduser elastic passwd elastic su elasticElasticsearch(简称ES)是一个基于Lucene的搜索服务器。它提供了一个分布式、多用户能力的全文搜索引擎,基于RESTful web…

ESLint的简单使用(js,ts,vue)

一、ESLint介绍 1.为什么要用ESLint 统一团队编码规范&#xff08;命名&#xff0c;格式等&#xff09; 统一语法 减少git不必要的提交 减少低级错误 在编译时检查语法&#xff0c;而不是等js引擎运行时才检查 2.eslint用法 可以手动下载配置 可以通过vue脚手架创建项…

学习threejs,使用AnimationMixer实现变形动画

&#x1f468;‍⚕️ 主页&#xff1a; gis分享者 &#x1f468;‍⚕️ 感谢各位大佬 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍⚕️ 收录于专栏&#xff1a;threejs gis工程师 文章目录 一、&#x1f340;前言1.1 ☘️THREE.AnimationMixer 动画…

嵌入式驱动开发详解1(系统调用)

文章目录 符设备驱动架构read函数详解用户层read函数内核层read函数 具体实现用户层代码 内核层代码细节分析 符设备驱动架构 如上图所示&#xff0c;应用层程序直接用系统提供的API函数即可调用驱动层相应的函数&#xff0c;中间的具体过程都是由linux内核实现的&#xff0c;…

算法.图论-习题全集(Updating)

文章目录 本节设置的意义并查集篇并查集简介以及常见技巧并查集板子(洛谷)情侣牵手问题相似的字符串组岛屿数量(并查集做法)省份数量移除最多的同行或同列石头 本节设置的意义 主要就是为了复习图论算法, 尝试从题目解析的角度,更深入的理解图论算法… 并查集篇 并查集简介以…

解决Ubuntu18.04及以上版本高分辨率下导致字体过小问题

解决Ubuntu18.04及以上版本高分辨率下导致字体过小问题 Chapter1 解决Ubuntu18.04及以上版本高分辨率下导致字体过小问题 Chapter1 解决Ubuntu18.04及以上版本高分辨率下导致字体过小问题 目前使用的是三星4K显示屏&#xff0c;屏幕分辨率太高了&#xff0c;导致VMWare Workst…

第27天 安全开发-PHP应用TP 框架路由访问对象操作内置过滤绕过核心漏洞

时间轴 演示案例 TP 框架-开发-配置架构&路由&MVC 模型 TP 框架-安全-不安全写法&版本过滤绕过 TP 框架-开发-配置架构&路由&MVC 模型 参考&#xff1a; https://www.kancloud.cn/manual/thinkphp5_1 1、配置架构-导入使用 去thinkphp官网可以看到&…

Mac的Terminal随机主题配置

2024年8月8日 引言 对于使用Mac的朋友&#xff0c;如果你是一个程序员&#xff0c;那肯定会用到Terminal。一般来说Terminal就是一个黑框&#xff0c;但其实Terminal是有10款官方皮肤。 每个都是不一样的主题&#xff0c;颜色和字体都会有所改变。现在就有一个方法可以很平均…

开源项目低代码表单设计器FcDesigner获取表单的层级结构与组件数据

在使用开源项目低代码表单设计器FcDesigner时&#xff0c;获取和理解表单的层级结构非常关键。通过getDescription和getFormDescription方法&#xff0c;您可以清晰掌握表单组件的组织结构和层次关系。这些方法为操控表单的布局和配置提供了强大的支持。 源码地址: Github | G…