百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术资源 > 正文

从服务器开发底层聊一聊协程的实现原理

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...

取消回复欢迎 发表评论: