基于C讲解协程设计原理

news2024/9/19 10:53:25

协程设计原理

背景

以epoll处理fd为例:

func () {
    while (1) {
        epoll_wait();
        for(;;) {
            recv();
            send();
        }
    }
}

在IO操作较为密集的情况下(网络IO和磁盘IO操作多,CPU计算少),由于检测到IO事件后,需要进行同步的IO操作,使得IO处理性能降低;因此,可以将这些recv()、send()操作交给异步线程(如消息队列+线程池):

thread_cb(void *arg) {
    recv();
    send();
}

func () {
    while (1) {
        epoll_wait();
        for (;;) { 
            push_other_thread();
        }
    }
}

然而,这样会不可避免地出现一个问题:同一个fd可能被多个线程处理,导致数据乱序,连接关闭了但数据未完全发送等问题。

协程

协程:同步的编程方式,异步的性能,适合做IO密集型任务

如浏览器需解析50个域名,请求DNS服务器时,同步的做法是将域名发送给DNS服务器后,等待结果返回再去发送其他域名的解析请求;另一种高效的同步方法是发送完请求后,将fd加入epoll,先去发送其他域名的解析请求,等到epoll检测到IO事件后再处理返回结果。

// 版本一:
while(idx++<50){
    send(fd);
    epoll_ctl(epfd,add,fd);

    int nready=epoll_wait(epfd);  // 这里把超时事件设置为0
    while(i++<nready){
        recv();
    }
}

进一步将IO检测和IO操作分开:

// 版本二:
// IO操作
while(idx++<50){
    <label1>     // 恢复:`resume`
    send(fd);
    epoll_ctl(epfd,add,fd);
    jump label2   // 跳转到lable2,相当于python中yield`原语`,goto只能在一个栈内跳转(函数内部跳转)
    while(i++<nready){
        recv();
    }
    jump label2
}

// IO检测
    <label2>
    int nready = epoll_wait(epfd);
    jump label1   // 跳转到label1

其中,epoll_wait()就相当于协程调度器,IO操作就是协程,一个协程让出,另一个协程恢复

实现上面的代码要解决的问题:切换(jump)怎么实现

  • C提供的标准接口:setjmp、longjmp
  • Linux提供的接口:ucontext
  • 自己汇编实现,本文利用汇编实现

首先,我们需要理解线程切换的概念:线程切换也只涉及基本的CPU上下文切换,也就是切换寄存器信息,而寄存器主要有:

  • 栈指针寄存器 %rsp:函数的栈顶指针就存储在这里
  • 指令指针寄存器 %rip:标识CPU运行的下一条指令
  • 栈帧指针寄存器 %rbp:栈帧指针,标识当前栈帧的起始位置
  • 状态寄存器 %rbx、%r12~%r15等:如标识CPU当前在用户态还是内核态下工作,以及进位、溢出状态

上下文可以说是一个程序的运行时切面,拿到了某一时刻的上下文,那就可以在这一时刻进行暂停与恢复;

而切换上下文分为两步:保存源寄存器值,将目的寄存器值加载到寄存器:

int _switch(nty_cpu_ctx *new_ctx, nty_cpu_ctx *cur_ctx);
// x86架构64位
__asm__ (
"    .text                                  \n"
"       .p2align 4,,15                                   \n"
".globl _switch                                          \n"
".globl __switch                                         \n"
"_switch:                                                \n"
"__switch:                                               \n"
"       # 保存第一个协程的寄存器的值                       \n"
"       movq %rsp, 0(%rsi)      # save stack_pointer     \n"   
"       movq %rbp, 8(%rsi)      # save frame_pointer     \n"
"       movq (%rsp), %rax       # save insn_pointer      \n"
"       movq %rax, 16(%rsi)                              \n"
"       movq %rbx, 24(%rsi)     # save rbx,r12-r15       \n"
"       movq %r12, 32(%rsi)                              \n"
"       movq %r13, 40(%rsi)                              \n"
"       movq %r14, 48(%rsi)                              \n"
"       movq %r15, 56(%rsi)                              \n"
"       # 将第二个协程的寄存器的值加载到寄存器              \n"
"       movq 56(%rdi), %r15                              \n"
"       movq 48(%rdi), %r14                              \n"
"       movq 40(%rdi), %r13     # restore rbx,r12-r15    \n"
"       movq 32(%rdi), %r12                              \n"
"       movq 24(%rdi), %rbx                              \n"
"       movq 8(%rdi), %rbp      # restore frame_pointer  \n"
"       movq 0(%rdi), %rsp      # restore stack_pointer  \n"
"       movq 16(%rdi), %rax     # restore insn_pointer   \n"
"       movq %rax, (%rsp)                                \n"
"       ret                                              \n"
);

线程切换和协程切换的区别:线程是需要操作系统调度的,也就是说首先要从用户态切换到内核态调用,然后有要从内核态切换到用户态执行,而协程只发生在用户态(感觉协程就像是个自己写的框架,一个线程在某一时刻只能运行一个协程,goroutine里单个线程中的协程也是并发的,如果要真正并行,需要多个线程或进程,也就是绑定不同核的协程才能真正地并行)

协程状态

20230130213914

在一个时间片内,在调度器里运行一段时间,在协程A运行一段时间,在调度器里运行一段时间,在协程B里运行一段时间,参考go,一个线程设置一个调度器

总体流程是:协程遇到IO操作yeild,回到调度器,调度器去resume协程

void nty_schedule_run(void) {

	nty_schedule *sched = nty_coroutine_get_sched();
	if (sched == NULL) return ;

	while (!nty_schedule_isdone(sched)) {
		
		// 1. expired --> sleep rbtree
		nty_coroutine *expired = NULL;
		while ((expired = nty_schedule_expired(sched)) != NULL) {
			nty_coroutine_resume(expired);     // 休眠的协程优先恢复
		}
		// 2. ready queue
		nty_coroutine *last_co_ready = TAILQ_LAST(&sched->ready, _nty_coroutine_queue);
		while (!TAILQ_EMPTY(&sched->ready)) {
			nty_coroutine *co = TAILQ_FIRST(&sched->ready);
			TAILQ_REMOVE(&co->sched->ready, co, ready_next);

			if (co->status & BIT(NTY_COROUTINE_STATUS_FDEOF)) {
				nty_coroutine_free(co);
				break;
			}

			nty_coroutine_resume(co);
			if (co == last_co_ready) break;
		}

		// 3. wait rbtree
		nty_schedule_epoll(sched);     
		while (sched->num_new_events) {
			int idx = --sched->num_new_events;
			struct epoll_event *ev = sched->eventlist+idx;
			
			int fd = ev->data.fd;
			int is_eof = ev->events & EPOLLHUP;
			if (is_eof) errno = ECONNRESET;

			nty_coroutine *co = nty_schedule_search_wait(fd);
			if (co != NULL) {
				if (is_eof) {
					co->status |= BIT(NTY_COROUTINE_STATUS_FDEOF);
				}
				nty_coroutine_resume(co);
			}
			is_eof = 0;
		}
	}

	nty_schedule_free(sched);
	
	return ;
}

协程入口函数中的让出部分代码:

static int nty_poll_inner(struct pollfd *fds, nfds_t nfds, int timeout) {

	if (timeout == 0)
	{
		return poll(fds, nfds, timeout);
	}
	if (timeout < 0)
	{
		timeout = INT_MAX;
	}

	nty_schedule *sched = nty_coroutine_get_sched();
	nty_coroutine *co = sched->curr_thread;
	
	int i = 0;
	for (i = 0;i < nfds;i ++) {
	
		struct epoll_event ev;
		ev.events = nty_pollevent_2epoll(fds[i].events);
		ev.data.fd = fds[i].fd;
		epoll_ctl(sched->poller_fd, EPOLL_CTL_ADD, fds[i].fd, &ev);

		co->events = fds[i].events;
		nty_schedule_sched_wait(co, fds[i].fd, fds[i].events, timeout);
	}
	nty_coroutine_yield(co);          // 遇到IO操作,加入epoll后让出(切换新的寄存器信息与协程)

	for (i = 0;i < nfds;i ++) {
	
		struct epoll_event ev;
		ev.events = nty_pollevent_2epoll(fds[i].events);
		ev.data.fd = fds[i].fd;
		epoll_ctl(sched->poller_fd, EPOLL_CTL_DEL, fds[i].fd, &ev);

		nty_schedule_desched_wait(fds[i].fd);
	}

	return nfds;
}

在网络IO中,一个协程对应一个fd是可行的,但是在磁盘IO中并不实用;其次,协程+epoll的实时性和只用epoll的实时性是差不多的;并且比reactor+epoll实时性要差

代码讲解

结构体定义

定义协程结构体和调度器结构体:

struct cpu_register_set {   // CPU寄存器
    void *r1;
    void *ebx;
    .......
};

// 队列宏定义
#define queue_node(name, type) struct name { \
    struct type *next;                  \
    struct type *prev;                  \
}


#define rbtree_node(name, type) struct name {
    char color;
    struct type *right;
    struct type *left;
    struct type *parent;
}

// 协程结构体
struct coroutine {
    struct cpu_register_set *set; //cpu寄存器组
    void *func;                 // 协程入口函数      ,参考pthread_create(),调用后不立即执行,只做两件事,创建task_struct* task结构体,将该结构体加入enqueue_ready就绪队列,然后由线程调度器调度
    void *arg;                  // 入口函数参数
    void *retval;               //  协程返回值

    void *stack_addr;           //  协程栈,协程一开始执行就将指针指向该栈栈顶,最好一个协程一个栈(独立栈),也可以采用共享栈 =》堆上的空间,栈结构
    size_t stack_size;           // 栈大小

    //struct coroutine *next;    // 最简单的做法,协程队列,不考虑协程状态,先到的IO先处理

    // 将协程按状态调度:新建、等待、就绪、睡眠、退出
    queue_node(ready_queue, coroutine) *ready;    // 就绪队列,用链表就ok      
    rbtree_node(coroutine) *sleep; // optional,跟时间的大小有关系,用红黑树且用时间做key,将超时节点置入就绪队列;不用最小堆,没有顺序,比如有两个节点超时了,小根堆只能一个一个取,太慢了
    rbtree_node(coroutine) *wait;  // 也跟时间有关系
 };


struct scheduler {    // 调度器

    struct scheduler_ops *ops;       // 调度策略
    struct coroutine *cur;         // 当前运行的协程,即调度的协程
    int epfd;     // epfd放到调度器中
    queue_node *ready_set;
    rbtree() *wait_set;
    rbtree() *sleep_set;
};

// 设置多个调度策略:如多状态运行、生产消费者模式
struct scheduler_ops {
    struct scheduler_ops *next;
    enset();
    deset();
}; 

API

API有两类,一类是对协程api的改造(参考线程api),一类是对网络api的改造(同步改异步+hook)

// 第一类
coroutine_create(entry_cb, arg);     // 创建协程

coroutine_join(coid, &ret) {       // 协程返回值,参考pthread_join()

    co = search(coid)              // 通过协程id获取协程
    while (co->ret == NULL) {
        wait();                    // 条件等待cond_wait();
    }

    return co->ret;
}

exec(co) {           // 获取返回值
    
    co->reval = co->func(co->arg);
    signal();
}

......

// 第二类:
// 同步IO api改异步,如accept(),send(),connection()等
accept_f(){
	int ret = poll(fd);   // 超时时间设置为0
	if ( ret>0){
		accept();
	}else{
		epoll_ctl(epfd,);
		yield();     // 先让出,等待被调用
	}
}

// 但是如果在项目中,很多地方都是调用的accept(),send(),难道要一个一个去更改函数名吗?  =》 hook

// hook:进程在启动时,会分配一块虚拟内存,其中的代码段中的accpet等动态链接库函数指针重定向到accpet_f等自定义函数,在调用accpet时,就是调用的我自己的accpet而不是系统的
accept_t accept_f = NULL;

int init_hook(void) {
	socket_f = (socket_t)dlsym(RTLD_NEXT, "socket");
	//read_f = (read_t)dlsym(RTLD_NEXT, "read");
	recv_f = (recv_t)dlsym(RTLD_NEXT, "recv");
	recvfrom_f = (recvfrom_t)dlsym(RTLD_NEXT, "recvfrom");

	//write_f = (write_t)dlsym(RTLD_NEXT, "write");
	send_f = (send_t)dlsym(RTLD_NEXT, "send");
    sendto_f = (sendto_t)dlsym(RTLD_NEXT, "sendto");

	accept_f = (accept_t)dlsym(RTLD_NEXT, "accept");
	close_f = (close_t)dlsym(RTLD_NEXT, "close");
	connect_f = (connect_t)dlsym(RTLD_NEXT, "connect");

}

int accept(int fd, struct sockaddr *addr, socklen_t *len) {

	if (!accept_f) init_hook();

	......
	
	return sockfd;
}

......

多核模式:协程要实现多核模式,需要借助多线程(CPU亲缘性)或多进程

github参考链接

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

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

相关文章

美颜sdk动态贴纸是什么?

美颜sdk如今已经成了广大视频拍摄平台的刚需&#xff0c;用户们也习惯了这种新颖的拍摄形式&#xff0c;原相机被无情“打入冷宫”&#xff0c;特别是短视频和直播平台中&#xff0c;绝大部分用户都在使用美颜sdk的趣味功能进行拍摄&#xff0c;“动态贴纸”就是一个非常好的例…

什么是PEPPOL BIS?

和常见的X12以及EDIFACT类似&#xff0c;PEPPOL BIS也是一种EDI标准&#xff0c;主要用于B2G和B2B交易&#xff0c;在欧盟的应用十分广泛。在业务场景中&#xff0c;PEPPOL不单单只是用于发票&#xff0c;从下单到开票流程中均可提供标准化的数据传输。 在此前的文章中&#x…

[Ansible系列]ansible tag介绍

简介 在大型项目当中&#xff0c;通常一个playbook会有非常多的task。而我们每次执行这个playbook时&#xff0c;都会将 所有task运行一遍。而事实上&#xff0c;在实际使用过程中&#xff0c;我们可能只是想要执行其中的一部分任务而已&#xff0c; 并不想把整个playbook完整跑…

【单链表】数据结构单链表的实现

前言&#xff1a;在之前的学习中我们已经了解了顺序表的相关知识内容&#xff0c;但是顺序表我们通过思考可以想到如下问题&#xff1a; 中间/头部的插入删除&#xff0c;时间复杂度为O(N)增容需要申请新空间&#xff0c;拷贝数据&#xff0c;释放旧空间。会有不小的消耗。增容…

性能技术分享|Jmeter+InfluxDB+Grafana搭建性能平台

一、引言最近在公司做性能技术分享时&#xff0c;发现同事对环境搭建能力&#xff0c;还是有些欠缺。或许&#xff0c;这也是大部分性能测试工程师所欠缺的技能。因为绝大部分的性能测试工程师&#xff0c;要么是使用测试开发架构师搭建的性能平台&#xff0c;要么自己使用Jmet…

【Unity3D】激光灯、碰撞特效

1 需求描述 本文将模拟激光灯&#xff08;或碰撞&#xff09;特效&#xff0c;详细需求如下&#xff1a; 从鼠标位置发射屏幕射线&#xff0c;检测是否与物体发生碰撞当与物体发生碰撞时&#xff0c;在物体表面覆盖一层激光灯&#xff08;或碰撞&#xff09;特效本文代码见→激…

振弦采集模块VMTool 配置工具的传感器数据读取

振弦采集模块VMTool 配置工具的传感器数据读取 连接传感器 将振弦传感器两根线圈引线分别连接到 VM 模块模块的 SEN和 SEN-两个管脚。 通常不分正负极&#xff0c;任意连接即可。 连接模块电源 使用 5V~12V 直流电源连接到 VM 模块的 VIN 和 GND&#xff0c;电源正极连接到 VIN…

【数据结构基础】树 - 平衡二叉树(AVL)

平衡二叉树&#xff08;Balanced Binary Tree&#xff09;具有以下性质&#xff1a;它是一棵空树或它的左右两个子树的高度差的绝对值不超过1&#xff0c;并且左右两个子树都是一棵平衡二叉树。平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等。 最小二叉平…

数学建模与数据分析 || 1. 数学建模简介

数学建模简介 文章目录数学建模简介1. 数学建模比赛的理解2. 一般数据分析的流程3. 机器学习与统计数据分析4. 各种编程软件仅仅是工具&#xff0c;对问题的观察视角和解决问题的策略才是关键2.1 数学建模的特点2.2 以 python&#xff08;jupyter notebook工作界面&#xff09;…

JSR303校验(表单参数校验)

1、maven坐标<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId><version>3.0.1</version> </dependency>2、校验规则3、定义好校验规则还需要开启校验&#…

用户区网络缓冲区

用户区网络缓冲区 为什么要有用户层缓冲区 TCP内核协议栈&#xff0c;每个连接都有一个接收缓冲区和一个发送缓冲区&#xff0c;为啥用户层也要有&#xff1a; 为啥要有接收缓冲区 生产者速度大于消费者速度&#xff1a;客户端发送地太快&#xff0c;服务器处理不过来&#…

SpringWebflux 执行流程和核心 API

SpringWebflux 基于 Reactor&#xff0c;默认使用容器是 Netty&#xff0c;Netty 是高性能的 NIO 框架&#xff0c;异步非阻 塞的框架 Netty_百度百科 (baidu.com)BIO、NIO、AIO_y_凉介的博客-CSDN博客_bin nio &#xff08;1&#xff09;Netty BIO 每一个请求过来会占用一个…

【系列05】类与对象 面向对象 封装继承多态 类 内部类

面向对象&#x1f601; 文章为本人随课程记录笔记形成 跟随老师"秦疆&#xff08;遇见狂神说)" 非常欢迎大家在文章下面留言评论互相交流,也欢迎大家有问题可以联系本人或者本人公众号 &#x1f609;学思则安 参考课程https://www.kuangstudy.com/course?cid1 有问…

Vue3通透教程【一】Vue3现状—必然趋势?

文章目录&#x1f31f; 专栏介绍&#x1f31f; Vue默认版本&#x1f31f; 拥抱Vue3的UI&#x1f31f; Vue3显著优势&#x1f31f; 专栏介绍 凉哥作为 Vue 的忠诚粉丝输出过大量的 Vue 文章&#xff0c;应粉丝要求开始更新 Vue3 的相关技术文章&#xff0c;Vue 框架目前的地位大…

现在什么款式运动无线耳机好、最适合运动的无线蓝牙耳机推荐

随着经济越来越好&#xff0c;人们的生活质量提高&#xff0c;我们对健康也是更加重视了。越来越多人开始“动起来”。健康运动&#xff0c;自然少不了专业的运动耳机。一副适合的运动耳机对我们锻炼身体有着事半功倍的作用&#xff0c;那么有哪些品牌值得推荐呢&#xff1f;小…

论文笔记(1):Large Language Models are few(1)-shot Table Reasoners

文章目录AbstractIntroductionRelated worksMethodExperimentdatasetbaselinesresultsmain resultsanalysisLimitationAbstract 已有研究表明&#xff0c;大型语言模型(LLM)在文本的少样本推理中表现excellent&#xff0c;本文证明LLM在表结构的f复杂少样本推理中表现也很comp…

【数据结构基础】树 - 二叉搜索树(BST)

本文主要介绍 二叉树中最基本的二叉查找树&#xff08;Binary Search Tree&#xff09;&#xff0c;&#xff08;又&#xff1a;二叉搜索树&#xff0c;二叉排序树&#xff09;它或者是一棵空树&#xff0c;或者是具有下列性质的二叉树&#xff1a; 若它的左子树不空&#xff0…

入门力扣自学笔记233 C++ (题目编号:2319)

2319. 判断矩阵是否是一个 X 矩阵 题目&#xff1a; 如果一个正方形矩阵满足下述 全部 条件&#xff0c;则称之为一个 X 矩阵 &#xff1a; 矩阵对角线上的所有元素都 不是 0 矩阵中所有其他元素都是 0 给你一个大小为 n x n 的二维整数数组 grid &#xff0c;表示一个正方形…

GitHub2022年十大热门编程语言榜单(上)

全球知名代码托管平台 GitHub发布的2022年GitHub Octoverse年度报告公布了全球最流行的十大编程语言&#xff0c;其中JavaScript蝉联第一&#xff0c;Python位列次席。 编程是技术革新的核心&#xff0c;对于所有的编程开发人员来说&#xff0c;对世界范围内编程语言发展和趋势…

搭WIFI拓扑有感

搭拓扑有感 人类革命&#xff0c;一场N*N的MIMO 关键技术&#xff1a;男女搭配 结婚生子 男女搭配&#xff1a;以搭档为单位调度&#xff0c;节省整体开资&#xff0c;克服短时间的寂寞 CP沟通&#xff1a;在说话间加一个保护间隔&#xff0c;不给对方太大的压力 结婚生子 …