从服务器开发底层聊一聊协程的实现原理
lipiwang 2024-11-01 14:10 9 浏览 0 评论
一、先介绍一组概念
进程
- 进程是系统进行资源分配和调度的基本单位
- 进程是一个实体,每一个进程都有自己地址空间
线程
- 线程是程序执行流的最小单元
- 一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成
- 线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源
子例程
- 子例程是某个主程序的一部分代码
- 子例程又被称为子程序、过程、方法、函数等。在主程序中可以调用子例程来执行
协程
- 协程与子例程一样,协程(coroutine)也是一种程序组件
- 协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行
- 注意,在一个子程序中中断,去执行其他子程序,不是函数调用,有点类似CPU的中断
二、同步与异步
- 在介绍协程之前,必须先要详细的介绍一下同步和异步,因为这是引出协程的重要原因
同步
- 以客户端为例,客户端向服务端发出请求之后,必须阻塞等待服务端给自己返回数据,属于一问一答的模式
- 下面是服务端同步的伪代码,核心是handle()函数:
- 服务端在接收到消息之后,必须阻塞等待处理完消息再去发送数据,这个过程必须是一次性完成的,并且是在一个线程中完成的
while (1) {
int nready = epoll_wait(epfd, events, EVENT_SIZE, -1);
for (i = 0;i < nready;i ++) {
int sockfd = events[i].data.fd;
if (sockfd == listenfd) {
int connfd = accept(listenfd, xxx, xxxx);
setnonblock(connfd);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = connfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
}
// 进行消息的处理
else {
handle(sockfd);
}
}
}
// 可以看到是数据的收发在一个过程中
int handle(int sockfd) {
// 先接收
recv(sockfd, rbuffer, length, 0);
// 消息处理
parser_proto(rbuffer, length);
// 再发送
send(sockfd, sbuffer, length, 0);
}
异步
- 异步与同步不同,在异步的情况下,客户端向服务端发出请求之后,客户端可以不等待服务端给自己返回数据,接着继续执行。当然接收回复这一过程在其他线程中执行,当回复达到之后再通知客户端去接收(一般由回调函数实现)
- 下面是服务端同步的伪代码,核心是handle()函数:
- 可以看到服务端在处理数据时,把任务交给了其他的线程去执行,而不必阻塞等待
while (1) {
int nready = epoll_wait(epfd, events, EVENT_SIZE, -1);
for (i = 0;i < nready;i ++) {
int sockfd = events[i].data.fd;
if (sockfd == listenfd) {
int connfd = accept(listenfd, xxx, xxxx);
setnonblock(connfd);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = connfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
}
// 进行消息的处理
else {
handle(sockfd);
}
}
}
// 此函数是在线程池创建的线程中运行
int thread_cb(int sockfd) {
recv(sockfd, rbuffer, length, 0);
parser_proto(rbuffer, length);
send(sockfd, sbuffer, length, 0);
}
// 此函数是在主线程中运行的
int handle(int sockfd) {
// 把读写任务放到另外的线程中去执行
push_thread(sockfd, thread_cb); //将 sockfd 放到其他线程中运行。
}
Linuxc/c++后台服务器开发高阶视频学习资料后台私信【架构】获取
备注
- "异步IO"和"IO异步"的区别:
- 异步IO:核心点是"IO",例如POSXI提供的AIO接口
- IO异步:核心点是"操作",例如上面服务端代码的异步处理,就是IO异步操作。我们平常所说的"多线程异步"指的就是这个
- "同步IO"和"IO同步"与上面也是相同的道理
同步与异步的区别
- 总结起来就是:
- 同步:编程简单,性能差
- 异步:编程复杂,性能高
- 同步编程简单是因为其数据的处理是在同一个过程中进行处理的。但是异步的处理可能在不同的线程中进行处理,例如在异步的情况下客户端发送2次请求,这2个请求可能在服务端的2个线程中进行处理,2个线程共用一个客户端的fd,可能造成数据混乱,最常见的解决方法就是进行加锁
四、设计协程的核心
- 通过上面的介绍,我们知道了同步与异步之间的差异,有没有一种方式,有异步性能,同步的代码逻辑。来方便编程人员对 IO 操作的 组件呢? 有,采用一种轻量级的协程来实现。在每次 send 或者 recv 之前进行 切换,再由调度器来处理 epoll_wait 的流程
- 而协程的设计核心目标就是:为了拥有同步的简单编程方式,同时又想要拥有异步的性能
五、实现协程的核心:跳转(协程切换)
- 协程想要拥有同步的编程方式和异步的性能,因此我们不能对同步的代码进行修改,而要想办法对异步的代码进行修改,使得其
- 下面我们以https://blog.csdn.net/qq_41453285/article/details/106357786中的HTTP客户端异步实现代码为例
- 下面且听我细细道来
如何跳转?往哪里跳转?
- 在代码中,客户端调用async_http_commit()函数向服务端发送一个HTTP请求,为了实现异步的方式,我们在调用send()发送数据之后,把这个fd添加到epoll中进行管理(这是异步的方式),而不是在send()后面调用recv()(这是同步的方式)
- 下面是async_http_thread_func()函数的代码,这个函数在while(1)循环中一直调用epoll_wait()监听描述符是否有事件发生,例如上面的async_http_commit()函数调用send()给服务端发送数据之后,服务端给客户端回送响应,那么epoll_wait()就会被触发,从而调用recv()接收数据
- 那么如何用异步代码实现同步的效果呢?那就需要程序进行跳转在
- 关于跳转可以详细见下面的解释(图片点开来看):
- (图左侧)async_http_commit()函数接收完数据之后为了实现与同步一样的效果,我们跳转到右侧的async_http_thread_func()函数
- (图右侧)跳转到async_http_thread_func()函数之后就可以调用epoll_wait()来检测数据了(因为上面的async_http_commit()函数调用了send()发送数据了),如果epoll_wait()检测到有数据来之后就可以在下面接收数据,当接收完数据之后再跳转回到左侧的async_http_commit()函数中
跳转的方法有哪些呢?
- 程序跳转的方法有3种:
- ①setjmp()、longjmp()函数:不推荐使用,编码复杂
- ②ucontext
- ③汇编实现
- 这3种方法,其中最常用的就是汇编,并且Go等语言的协程也是用汇编实现的
yield、resume
- yield:让出当前的CPU,跳转到指定的位置进行执行,这个过程叫做yield
- resume:上面yield让出CPU之后跳转到指定的位置执行,当指定的位置执行完成之后,回到当初的位置这个过程叫做resume
- 例如,对于上面来说,就是
六、如何通过汇编实现协程的切换呢?
- 下载代码https://github.com/wangbojing/NtyCo开始解析,后面要用到
- 对于上面的跳转来说,其实就是协程之间的切换,如何实现这种切换呢?
- 在介绍协程切换之前,来说一下线程的切换,如下图所示:
- CPU有很多的寄存器,这些寄存器保存了当先在处理器上运行线程的信息
- 例如当前CPU运行的是A线程,那么寄存器保存的都是线程A的信息
- 当此时需要把线程A切出CPU,来让B线程在CPU上运行,那么就需要把当前寄存器的内容都保存在线程A的栈中,然后把B运行所需要的内容加载到寄存器中,从而使得B线程在CPU上运行起来
- 协程如何切换?与线程是相同的道理,还是以上面的图片为例:
- sync_http_commit()函数调用send()之后,在跳转到pos位置之前把当前寄存器的内容保存到一个结构体中(例如命名为store结构体)
- 跳转到async_http_thread_func()函数指定的pos位置之后,把pos位置的内容信息加载到寄存器中开始执行
如何实现这些内容的保存与切换
- 首先到代码的Nty_coroutine.c文件中找到_switch()函数,这个函数是实现切换的核心:
- 参数1:新的上下文
- 参数2:当前的上下文
- _switch()函数就是把当前寄存器的内容保存在参数2中,然后加载参数1所指定的内容加载到寄存器中
- 例如,如果是上面的sync_http_commit()函数,其在"jump-pos"的时候就调用_switch()进行切换,然后async_http_thread_func()函数执行完需要切换回sync_http_commit()函数的时候,会在"jump->back"的地方调用这个函数,只是参数不同而已
- nty_cpu_ctx结构体就是一些寄存器的指针
- _switch()函数可以用下面的图来表示
- 那么如何实现这些寄存器值的保存与交换呢?
- 以下面为例,自己看图片吧,稍微有点复杂
- _switch()函数的参数1名为rdi、参数2名为rsi
- 在左侧,前一半部分:汇编指令把寄存器的内容保存到rsi中,留下次跳转回来使用;后一半部分:把rdi的内容加载到寄存器中开始使用
- 0、8、16那些是偏移,因为一个指针就是8字节,所以rsp对应的是esp、rbp对应的是ebp......依次类推
- X86-64有16个64位寄存器,分别是:
- %rax:作为函数返回值使用
- %rsp:栈指针寄存器,指向栈顶
- %rdi,%rsi,%rdx,%rcx,%r8,%r9:用作函数参数,依次对应第1参数,第2参数......依次类推(例如在_switch()函数中,参数1叫做rdi、参数2叫做rsi......)
- %rbx,%rbp,%r12,%r13,%14,%15:用作数据存储,遵循被调用者使用规则,简单说就是随便 用,调用子函数之前要备份它,以防他被修改 %r10,%r11 用作数据存储,遵循调用者使用规则,简单说就是使用之前要先保存原值
相关推荐
- ubuntu单机安装open-falcon极度详细操作
-
备注:以下操作均由本人实际操作并得到验证,喜欢的同学可尝试操作安装。步骤一1.1环境准备(使用系统:ubuntu18.04)1.1.1安装redisubuntu下安装(参考借鉴:https://...
- Linux搭建promtail、loki、grafana轻量日志监控系统
-
一:简介日志监控告警系统,较为主流的是ELK(Elasticsearch、Logstash和Kibana核心套件构成),虽然优点是功能丰富,允许复杂的操作。但是,这些方案往往规模复杂,资源占用高,...
- 一文搞懂,WAF阻止恶意攻击的8种方法
-
WAF(Web应用程序防火墙)是应用程序和互联网流量之间的第一道防线,它监视和过滤Internet流量以阻止不良流量和恶意请求,WAF是确保Web服务的可用性和完整性的重要安全解决方案。它...
- 14配置appvolume(ios14.6配置文件)
-
使用AppVolumes应用程序功能,您可以管理应用程序的整个生命周期,包括打包、更新和停用应用程序。您还可以自定义应用程序分配,以向最终用户提供应用程序的特定版本14.1安装appvolume...
- 目前流行的缺陷管理工具(缺陷管理方式存在的优缺点)
-
摘自:https://blog.csdn.net/jasonteststudy/article/details/7090127?utm_medium=distribute.pc_relevant.no...
- 开源数字货币交易所开发学习笔记(2)——SpringCloud
-
前言码云(Gitee)上开源数字货币交易所源码CoinExchange的整体架构用了SpringCloud,对于经验丰富的Java程序员来说,可能很简单,但是对于我这种入门级程序员,还是有学习的必要的...
- 开发JAX-RPC Web Services for WebSphere(下)
-
在开发JAX-RPCWebServicesforWebSphere(上)一文中,小编为大家介绍了如何创建一个Web服务项目、如何创建一个服务类和Web服务,以及部署项目等内容。接下来小编将为大...
- CXF学习笔记1(cxf client)
-
webservice是发布服务的简单并实用的一种技术了,个人学习了CXF这个框架,也比较简单,发布了一些笔记,希望对笔友收藏并有些作用哦1.什么是webServicewebService让一个程序可...
- 分布式RPC最全详解(图文全面总结)
-
分布式通信RPC是非常重要的分布式系统组件,大厂经常考察的Dubbo等RPC框架,下面我就全面来详解分布式通信RPC@mikechen本篇已收于mikechen原创超30万字《阿里架构师进阶专题合集》...
- Oracle WebLogic远程命令执行0day漏洞(CVE-2019-2725补丁绕过)预警
-
概述近日,奇安信天眼与安服团队通过数据监控发现,野外出现OracleWebLogic远程命令执行漏洞最新利用代码,此攻击利用绕过了厂商今年4月底所发布的最新安全补丁(CVE-2019-2725)。由...
- Spring IoC Container 原理解析(spring中ioc三种实现原理)
-
IoC、DI基础概念关于IoC和DI大家都不陌生,我们直接上martinfowler的原文,里面已经有DI的例子和spring的使用示例《InversionofControlContainer...
- Arthas线上服务器问题排查(arthas部署)
-
1Arthas(阿尔萨斯)能为你做什么?这个类从哪个jar包加载的?为什么会报各种类相关的Exception?我改的代码为什么没有执行到?难道是我没commit?分支搞错了?遇到问题无法在...
- 工具篇之IDEA功能插件HTTP_CLENT(idea2021插件)
-
工具描述:Java开发人员通用的开发者工具IDEA集成了HTTPClient功能,之后可以无需单独安装使用PostMan用来模拟http请求。创建方式:1)简易模式Tools->HTTPCl...
- RPC、Web Service等几种远程监控通信方式对比
-
几种远程监控通信方式的介绍一.RPCRPC使用C/S方式,采用http协议,发送请求到服务器,等待服务器返回结果。这个请求包括一个参数集和一个文本集,通常形成“classname.meth...
- 《github精选系列》——SpringBoot 全家桶
-
1简单总结1SpringBoot全家桶简介2项目简介3子项目列表4环境5运行6后续计划7问题反馈gitee地址:https://gitee.com/yidao620/springbo...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- maven镜像 (69)
- undefined reference to (60)
- zip格式 (63)
- oracle over (62)
- date_format函数用法 (67)
- 在线代理服务器 (60)
- shell 字符串比较 (74)
- x509证书 (61)
- localhost (65)
- java.awt.headless (66)
- syn_sent (64)
- settings.xml (59)
- 弹出窗口 (56)
- applicationcontextaware (72)
- my.cnf (73)
- httpsession (62)
- pkcs7 (62)
- session cookie (63)
- java 生成uuid (58)
- could not initialize class (58)
- beanpropertyrowmapper (58)
- word空格下划线不显示 (73)
- jar文件 (60)
- jsp内置对象 (58)
- makefile编写规则 (58)