首页 > 编程知识 正文

协程的实现,golang协程原理

时间:2023-05-03 20:21:41 阅读:119629 作者:181

协程在了解协程前,我们需要先理清几个概念:同步,异步,阻塞,非阻塞

同步vs异步和异步描述了用户线程与内核的交互方式

同步是指用户线程启动I/o请求后,必须等待或轮询才能执行内核I/o操作。 这意味着在用户线程启动I/o请求后将继续运行。 介绍如何在内核I/o操作完成后通知用户线程,或调用在用户线程中注册的回调函数阻止内核I/o操作。 其中,同步区分块和非块,异步必须是非块

块—必须在I/o操作完全完成后才能返回到用户空间中的非块。 调用I/o操作后立即向用户返回状态值。 不需要等待I/o操作完全完成。在理清了同步,异步,阻塞,非阻塞的概念后,我们接下来对比看下IO同步和IO异步的处理流程

IO同步vs IO异步IO同步: IO发现和IO读写是一个过程,在IO操作过程中,主过程被阻止,不能处理其他业务。 同步处理CPU资源利用率低的//伪代码int mainloop () while) int nready=epoll _ waaa dy//io发现for(I=0); i nready; I ) (/io读写recv(sockfd,rbuffer,length,0 ); 帕Parser(rbuffer,length ); send(sockfd,sbuffer,length,0 ); }}IO异步: IO检测和IO读写不在一个进程中。 发现IO准备就绪时,创建线程进行IO读写。 检测线程不会被阻止。 对CPU的资源利用率高//异步处理伪代码voidthread_CB(intsockfd ) /此函数在线程池创建的线程中运行。 handle是指在一个线程的上下文中不运行recv(sockfd、rbuffer、length、0 )的parse(rbuffer、length ); send(sockfd,sbuffer,length,0 ); }int mainloop () while )1) { int nready=epoll_wait )…); 检测//io for (I=0; i nready; I ) push_Thread(sockfd,thread_cb ); }}总结:同步处理流程的优点:套接字管理方便,程序逻辑清晰,符合人类思维的缺点。 IO检测与IO读写相同过程、响应时间长、程序性能低的异步处理过程的优点:子模块逻辑清晰、IO检测与IO读写分离、响应时间短、程序性能高的缺点:多线程为了解决这种情况,必须使用各软盘

Linux系统的跳转方法setjmp/longjmp :长跳转,可以跨函数堆栈跳转,但只能在进程内部跳转。 不能跨越进程(c接口实现) ucontext (可以在进程中的上下文之间跳转)由Linux系统提供的接口)通过用汇编指令操作CPU寄存器来实现进程中的上下文

APP切换原理APP切换原理:暂时保存CPU中当前执行APP的上下文寄存器的值,将下一次执行APP之前的上下文寄存器加载到CPU的对应寄存器中,从而完成APP切换下图:

协和式将切换的操作封装在两个原语操作yield中。 调用后,此函数不会立即返回。 相反,切换到最近运行resume的上下文resume。 调用后,此函数也不会立即返回。 相反,切换到要执行联合实例的yield的位置的resume和yield是两个可逆过程的原子操作_switch操作。 yield和resume两个基元操作的内部实现都在_switch上实现跳转,_switch函数是与程序集中实现的new_ctx相对应的寄存器指针rdicut_ctx

协和式定义协和式的结构包括:协和式运行体、协和式调度器

IO同步和IO异步都有各自的优缺点,那么是否有解决方来使得代码既有同步的简单编程方式,又能实现异步的高性能?答案是有的,我们可以在同步的代码实现基础加上跳转操作,即当调用完阻塞式的IO操作后,我们可以使用跳转操作将CPU切换到其他IO就绪的子流程上执行,以提高CPU的利用率,使得同步做得跟异步差不多性能

当前执行体上下文: cpu_ctx,用于仲裁的、主要存储CPU寄存器值仲裁子进程的回调函数: func (,回调函数

数参数:arg栈空间:协程内部函数调用时压栈用的栈空间大小:协程创建的时间点当前运行状态:协程ID调度器的全局对象就绪状态节点:ready,就绪集合中的元素等待状态节点:wait,等待集合中的元素休眠状态节点:sleep,休眠集合中的元素

调度器的定义:(用来管理协程或者协程统一的属性定义在调度器中)

CPU的寄存器上下文:协程创建的时间点当前运行的协程:方便yield操作 yield(sched->cur, sched->cur->next)epoll句柄 epfd: epoll是协程调度器的核心驱动epoll监听的事件集:epoll_events就绪集合:由于协程优先级一致,所以使用队列进行存储休眠集合:由于休眠集合需要按照睡眠时长进行排序,所以采用红黑树来存储,key为睡眠时长,value为对应的协程节点等待集合:等待集合存储的是等待IO就绪,等待IO也是有时长的,所以也是采用红黑树来存储,key为等待时长,value为对应的协程节点

协程内部数据集合的关系

协程的工作流程 创建协程: coroutine_create() 创建协程创建完后,加入就绪队列中 IO异步操作: 将 sockfd 添加到 epoll 管理中进行上下文环境切换, 由协程上下文 yield 到调度器的上下文调度器获取下一个协程上下文, resume 到新的协程IO异步操作的上下文切换时序图如下

回调协程子过程: CPU有个非常重要的寄存器叫EIP,用来存储CPU下一条指令的地址将回调函数的地址存储在EIP中,将相应的参数存储到相应的参数寄存器中,实现子过程调用的逻辑代码如下: void _exec(nty_coroutine *co) { co->func(co->arg); //子过程的回调函数}void coroutine_init(nty_coroutine *co) { //ctx 就是协程的上下文 co->ctx.edi = (void*)co; //设置参数 co->ctx.eip = (void*)_exec; //设置回调函数入口 //当实现上下文切换的时候,就会执行入口函数_exec , _exec 调用子过程 func} 协程的接口封装

协程的接口封装可分为两类:

类1;所有需要判断IO是否就绪的IO操作,将同步操作封装成异步操作

具体接口: connect();accept();send()/write()/sendto();recv()/read()/recvfrom(); 封装样式如下: nty_func(){ epoll_ctl(add, fd); //fd先加入epoll //然后yield让出cpu,跳到调度器中,由调度器找询下一个执行的协程,然后调用resume跳到对应协程中 yield(); func();} 封装方法: 可以给func加前缀可以使用hook方法,截获并自定义系统接口

类2:协程执行流程的接口

具体接口: 创建协程: coroutine_create(…);调度器调度:scheduler_loop(…); 协程的调度 协程的调度器实现方案有两种:一种是生产者消费者模式:,另一种是多状态模式生产者消费者模式:

多状态模式:

协程的多核模式实现 多核的模式 线程的粘合进程的粘合:将指定的进程绑定到指定的cpu核上执行,实现CPU的亲缘性 实现多核的方式 借助多线程 所有线程共用一个调度器 会出现线程间互跳需对调度器需要加锁 每个线程对应一个调度器 (性能较优) 不需要加锁,但成本较大 适用场景:前后请求间存在共享资源或者依赖,则使用多线程模式。如即时聊天系统 借助进程 实现代码简单每个进程对应一个调度器适用场景:前后请求间无共享资源或者依赖,则使用多进程模式。 如nginx、http请求场景 用汇编实现:实现起来较复杂,此处不展开 协程的分类 有栈协程:每一个协程,有独立的栈 优点:实现容易,性能高缺点:栈利用率不高 无栈协程:共享栈 优点:栈利用率高缺点,实现复杂 协程库 libgo/libco: c++实现的ntyco:c实现 epoll实现异步的两种方案对比

方案1:多线程(线程池)+ epoll

线程1:不停的提交请求,提交完请求的连接,加入到epoll中线程2:由 epoll_wait 被动等待结果返回

方案二:协程 + epoll

提交请求,让出CPU,切换到另一个线程的epoll中由epoll来检测IO是否有数据可读,若有则recv数据,然后切换回当前操作

对比:方案2会比方案1慢一些,慢主要来源于调度器,但方案2编程简单好维护

线程 vs 协程性能对比 IO密集型场景:协程能代替线程计算密集型场景:协程和线程无差别

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。