协程的设计原理与汇编实现

主要通过以下10个方面来了解协程的原理:

  1. 为什么会有协程,协程解决什么问题?
  2. 协程的原语
  3. 协程的切换
  4. 协程的运行流程
  5. 协程的结构体定义
  6. 协程调度的策略
  7. 协程调度器如何定义
  8. 协程api的实现,hook
  9. 协程的多核模式
  10. 协程如何测试

本文主要介绍1-4,6-10会在《协程调度器实现与性能测试》中介绍。

为什么要有协程?协程解决了什么问题?

关于协程,我们经常看到这样的话:同步的编程方式,异步的性能。那么什么是同步,什么是异步呢?

同步与异步

同步和异步,是形容两者之间的关系。两者在一个流程内,就是同步;两者不在一个流程内,就是异步。

我们这里说的同步和异步,是指io同步操作和io异步操作。

还有一个容易与io异步操作混淆的概念,异步io,就是指有io数据的时候,直接callback,AIO, 比如boost的asio;

服务器io同步操作与io异步操作

对于服务器而言,io检测和io操作在一个流程内,就是同步;io检测和io操作不在一个流程内,就是异步。

服务器io同步操作代码如下:

func () {

    while (1) {

        epoll_wait();

        for(;;) {

            recv();

            send();

        }

    }

}

io操作(send, recv)与epoll_wait在同一个处理流程里面。这就是io同步操作。

优点:

  1. fd管理方便;
  2. 代码逻辑清晰,实现简单。

缺点:

epoll_wait和io操作在同一流程,程序性能差。

异步代码如下:

thread_cb(void *arg) {

    poll() // 判断fd是否真的可读。
    recv();

    send();

}

func () {

    while (1) {

        epoll_wait();

        for (;;) {
            
            push_other_thread(); // 通过thread_cb处理io

        }

    }

}

使用其他线程处理io操作(recv, send),使得io操作与epoll_wait解耦。这就叫io异步操作。

优点:

程序性能高。

缺点:

同一个fd可能被多个线程处理,fd的管理就会比较麻烦,避免在io操作的时候,fd出现关闭或者其他异常。

客户端io同步操作与io异步操作

对于客户端而言,send和recv在同一个流程,就是io同步操作;send和recv不在同一个流程里面,就是io异步操作。

mysql/redis的协议,都是request-reply的模式。客户端发送request后,当前线程挂起,等待response。这就是典型的io同步操作。我们在之前的文章《异步请求实现》中介绍过异步请求的实现,当前线程send数据后,在另一个线程中使用epoll_wait进行io检测,io就绪后,进行recv,这就是一个io异步操作。

客户端如果有大量连接,需要发送大量请求,使用io异步操作,也会提高程序性能。

无论是服务器,还是客户端的io异步操作,性能都比io同步操作要高很多,但是代码的逻辑比同步要复杂。

协程解决了什么问题?

有没有一种方式,有异步的性能,同步的代码逻辑,来方便程序员对io操作的组件呢?有,采用一种轻量级的协程来实现。协程解决了io操作程序复杂程度和性能之间的矛盾。写代码的方式是同步的,底层运行的逻辑是异步的。

以客户端代码为例,使用协程进行编程的时候,就可以将以下代码变成异步的。

{
    send();
    recv();
}

这个代码是怎么变成异步执行的呢?

在recv、send之前,先将fd加入到epoll,之后进行一个switch操作,让出CPU,切换到epoll检测,检测到有io就绪,再进行一次switch,切换到就绪io对应的协程继续执行。epoll_wait一定意义上就是协程调度器,io操作就可以做成协程的感觉。

img

协程的原语

协程的两个原语操作:yield, resume.

yield,协程将CPU控制权让给调度器;

resume,调度器调度协程继续运行。

img

协程的切换

yield、resume如何实现?

都是通过switch(A, B)实现协程的切换

协程中跳转实现方式:
1)setjmp/logjmp C标准方法, 容易理解,但是维护的时候可读性差。在《手动实现try-catch组件》中使用了这种方式实现跳转。
2)ucontext,linux系统提供的
3)汇编,在协程中保存CPU寄存器,再将即将运行的协程的上下文寄存器mov到对应的寄存器上。

我们使用汇编的方法实现切换。

以线程为例,线程的切换方法如下:

img

协程的切换也是类似的,首先在当前运行的协程中保存CPU寄存器,再将即将运行的协程的上下文寄存器mov到对应的寄存器上。需要参考x86-64寄存器手册实现。

协程的运行流程

img

协程中遇到io操作,就加入到epoll里面,yield,将CPU让出,回到调度器,调度器进行调度,决定哪个协程运行。

一个fd对应一个协程的设计方法,是不是最优的?能不能设计成多分fd对应一个协程?

对于网络框架,一个fd对应一个协程是一个很好的方案;

如果是对界面刷新或者磁盘文件操作,就不是很合适。

比如A协程 recv,如果该fd io已经准备就绪了,这时候yield,调度器会调度其他协程运行,可能调度几百几千个其他协程,最后再回到A协程进行recv,它的实时性有没有意义?

对于大量io,所有io一起看的话,单个io的实时性是没有意义的。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值