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

news/2024/11/16 21:21:30/

文章目录

  • 为什么使用协程
  • 协程的理解
  • 协程优势
  • 协程的原语操作
    • 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


http://www.ppmy.cn/news/1547545.html

相关文章

【免越狱】iOS砸壳 可下载AppStore任意版本 旧版本IPA下载

软件介绍 下载iOS旧版应用&#xff0c;简化繁琐的抓包流程。 一键生成去更新IPA&#xff08;手机安装后&#xff0c;去除App Store的更新检测&#xff09;。 软件界面 支持系统 Windows 10/Windows 8/Windows 7&#xff08;由于使用了Fiddler库&#xff0c;因此需要.Net环境…

CAP与BASE分布式理论

CAP理论 C&#xff1a;Consistency 一致性&#xff1a;指强一致性&#xff0c;分布式系统中的所有节点在同一时刻具有同样的值、都是最新的数据副本&#xff0c;一致性保证了不管向哪台服务器写入数据&#xff0c;其他的服务器能实时同步数据 强一致性&#xff1a;写入数据的时…

C++11新特性:lambda表达式,包装器,新的类功能

1. lambda表达式 1.1 基本语法 lambda表达式本质上是一个匿名函数对象&#xff0c;但是和普通函数不一样他可以定义在函数内部。 lambda表达式使用层而言没有类型&#xff0c;所以我们一般使用auto或者模板参数定义的对象去接收lambda对象。 lambda表达式的格式如下&#x…

高级java每日一道面试题-2024年11月04日-Redis篇-Redis如何做内存优化?

如果有遗漏,评论区告诉我进行补充 面试官: Redis如何做内存优化? 我回答: 在Java高级面试中&#xff0c;关于Redis如何做内存优化的问题&#xff0c;可以从以下几个方面进行详细解答&#xff1a; 一、Redis内存优化概述 Redis内存优化主要是指通过一系列策略和技术&#…

数据结构-二叉树及其遍历

🚀欢迎来到我的【数据结构】专栏🚀 🙋我是小蜗,一名在职牛马。🐒我的博客主页​​​​​​ ➡️ ➡️ 小蜗向前冲的主页🙏🙏欢迎大家的关注,你们的关注是我创作的最大动力🙏🙏🌍前言 本篇文章咱们聊聊数据结构中的树,准确的说因该是只说一说二叉树以及相…

蓝桥杯每日真题 - 第13天

题目&#xff1a;&#xff08;删边问题&#xff09; 题目描述&#xff08;14届 C&C B组F题&#xff09; 解题思路&#xff1a; 图的构建&#xff1a;使用邻接链表表示图&#xff0c;边的起点和终点分别存储在数组中&#xff0c;以支持高效的遍历。 Tarjan算法&#xff1a…

SQL Server 查询设置 - LIKE/DISTINCT/HAVING/排序

目录 背景 一、LIKE - 模糊查询 1. 通配符 % 2. 占位符 _ 3. 指定集合 [] 3.1 表示否定 ^ 3.2 表示范围 - 4. 否定 NOT 二、DISTINCT - 去重查询 三、HAVING - 过滤查询 四、小的查询设置 1. ASC|DESC - 排序 2. TOP - 限制 3. 子查询 4. not in - 取补集&…

STM32完全学习——F407ZGT6点亮LED

一、寄存器描述 我们想要点亮LED&#xff0c;无非就是对于寄存器的一些设置&#xff0c;主要分为两步&#xff0c;首先是需要打开相应GPIO的时钟&#xff0c;这是因为STM32在上电后&#xff0c;每个外设的时钟默认都是关闭的&#xff0c;需要我们手动打开。其次就是对GPIO的一…