协程的实现原理

协程

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

同步 vs 异步

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

  • 同步:指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行
  • 异步:指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程或者调用用户线程注册的回调函数

阻塞 vs 非阻塞

阻塞和非阻塞描述的是用户线程调用内核IO操作的方式,其中同步才区分阻塞和非阻塞,异步则一定是非阻塞

  • 阻塞:指IO操作需要彻底完成后才返回到用户空间
  • 非阻塞:指IO操作被调用后立即返回给用户一个状态值,无需等待IO操作彻底完成

在理清了同步,异步,阻塞,非阻塞的概念后,我们接下来对比看下IO同步和IO异步的处理流程

IO同步 vs IO异步

  • IO同步:IO检测和IO读写在一个流程中,整个IO操作过程中,主进程是被阻塞的,不能处理其他业务,对CPU的资源利用率低
//同步处理伪代码
int mainloop() {
    while(1) {
        int nready = epoll_wait(...);    //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的资源利用率高
//异步处理伪代码
void thread_cb(int sockfd)
{
    // 此函数是在线程池创建的线程中运行。
    // 与 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);
        }
    }
}
  • 总结:
    • 同步处理流程
      • 优点:socket管理方便,程序逻辑清晰,符合人的思维
      • 缺点:IO检测与IO读写在同一流程,响应时间长,程序性能低
    • 异步处理流程
      • 优点:子模块逻辑清晰,IO检测与IO读写分离,响应时间短,程序性能高
      • 缺点:多线程共同管理fd容易造成读写异常,为了解决这种情况就必须在使用每个fd前加锁,但是这样做的效率较低

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

Linux系统的跳转方法

  • setjmp/longjmp:长跳,可以实现跨越函数栈间的跳转,但是只能在进程内部跳转,不能跨进程 (C接口实现)
  • ucontext:可以实现进程内上下文间的跳转(linux系统提供的接口)
  • 自己使用汇编实现:通过汇编指令操作CPU寄存器,来实现进程中上下文间的切换 (协程就是使用此方式)

单独调用跳转方法难度高且容易出错,是否可将跳转方法封装成代码框架,于是乎协程就产生了,在了解协程前,我们先看下协程的切换原理是如何的

协程切换原理

  • 协程切换的原理:将CPU中当前运行协程的上下文寄存器值暂时保存,然后再将即将运行协程的上下文寄存器加载到CPU中相应的寄存器上,从而完成协程切换,如下图所示:

image

  • 协程将切换的操作封装成两个原语操作
    • yield:调用后该函数不会立即返回, 而是切换到最近执行 resume 的上下文
    • resume:调用后该函数也不会立即返回,而是切换到运行协程实例的 yield 的位置
    • resume 与 yield 是两个可逆过程的原子操作
    • _switch操作:yieldresume 两个原语操作的内部实现都是通过 _switch 实现跳转, _switch函数是通过汇编实现的
      • new_ctx 对应的寄存器指针 rdi
      • cut_ctx 对应的寄存器指针 rsi

image
image

协程的定义

协程的组成包含: 协程运行体,协程调度器

  • 运行体的定义:(协程特有的属性定义在运行体中)

    • 当前运行体上下文: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为对应的协程节点
  • 协程内部数据集合的关系

image

协程的工作流程

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

image

  • 回调协程子过程:
    • 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(…);

协程的调度

  • 协程的调度器实现方案有两种:一种是生产者消费者模式:,另一种是多状态模式
  • 生产者消费者模式:

image

  • 多状态模式:

image

协程的多核模式实现

  • 多核的模式
    • 线程的粘合
    • 进程的粘合:将指定的进程绑定到指定的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密集型场景:协程能代替线程
  • 计算密集型场景:协程和线程无差别
  • 7
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Kotlin协程是一种轻量级的并发框架,它可以在不创建新线程的情况下实现异步操作。Kotlin协程实现原理是基于挂起函数和Continuation(协程上下文)。 在Kotlin协程中,挂起函数是指可以被暂停执行,并在稍后继续执行的函数。在挂起函数中,可以使用`suspend`关键字来标记该函数为挂起函数。当调用一个挂起函数时,该函数会返回一个`Continuation`对象,该对象可以被用来在稍后的时间点恢复挂起函数的执行。 Kotlin协程的调度器会在适当的时候调用Continuation对象的`resume`方法来恢复挂起函数的执行。当一个挂起函数被恢复执行时,它会从上一次挂起的地方继续执行,直到函数结束或者再次遇到挂起点。 Kotlin协程实现原理可以用以下伪代码来说明: ```kotlin fun main() { launch { println("Hello") delay(1000) println("World") } } suspend fun delay(time: Long) { // 挂起当前协程,等待一段时间 // 通过Continuation对象来恢复协程的执行 suspendCoroutine<Unit> { continuation -> Timer().schedule(time) { continuation.resume(Unit) } } } fun launch(block: suspend () -> Unit) { // 创建一个新的协程,并将其加入到调度器中 val coroutine = Coroutine(block) coroutine.start() } class Coroutine(private val block: suspend () -> Unit) { fun start() { block.startCoroutine(this) } } class Continuation(private val coroutine: Coroutine) { fun resume(value: Any?) { coroutine.resume(value) } } suspend fun <T> suspendCoroutine(block: (Continuation<T>) -> Unit): T { // 挂起当前协程,等待Continuation对象被调用 // 通过Continuation对象来恢复协程的执行 return suspendCoroutineOrReturn { continuation -> block(Continuation(coroutine)) } } ``` 在上面的代码中,`launch`函数用于创建一个新的协程,并将其加入到调度器中。`Coroutine`类表示一个协程,`start`方法用于启动协程的执行。`suspendCoroutine`函数用于挂起当前协程,并等待Continuation对象被调用以恢复协程的执行。`delay`函数使用`suspendCoroutine`函数来实现挂起功能,并在一定时间后恢复协程的执行。 Kotlin协程实现原理比较复杂,但是开发者只需要关注如何使用协程来实现异步操作即可。通过使用协程,开发者可以编写出更加简洁、易于理解、易于维护的异步代码。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值