主要通过以下10个方面来了解协程的原理:
- 为什么会有协程,协程解决什么问题?
- 协程的原语
- 协程的切换
- 协程的运行流程
- 协程的结构体定义
- 协程调度的策略
- 协程调度器如何定义
- 协程api的实现,hook
- 协程的多核模式
- 协程如何测试
本文主要介绍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同步操作。
优点:
- fd管理方便;
- 代码逻辑清晰,实现简单。
缺点:
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操作就可以做成协程的感觉。
协程的原语
协程的两个原语操作:yield, resume.
yield,协程将CPU控制权让给调度器;
resume,调度器调度协程继续运行。
协程的切换
yield、resume如何实现?
都是通过switch(A, B)实现协程的切换
协程中跳转实现方式:
1)setjmp/logjmp C标准方法, 容易理解,但是维护的时候可读性差。在《手动实现try-catch组件》中使用了这种方式实现跳转。
2)ucontext,linux系统提供的
3)汇编,在协程中保存CPU寄存器,再将即将运行的协程的上下文寄存器mov到对应的寄存器上。
我们使用汇编的方法实现切换。
以线程为例,线程的切换方法如下:
协程的切换也是类似的,首先在当前运行的协程中保存CPU寄存器,再将即将运行的协程的上下文寄存器mov到对应的寄存器上。需要参考x86-64寄存器手册实现。
协程的运行流程
协程中遇到io操作,就加入到epoll里面,yield,将CPU让出,回到调度器,调度器进行调度,决定哪个协程运行。
一个fd对应一个协程的设计方法,是不是最优的?能不能设计成多分fd对应一个协程?
对于网络框架,一个fd对应一个协程是一个很好的方案;
如果是对界面刷新或者磁盘文件操作,就不是很合适。
比如A协程 recv,如果该fd io已经准备就绪了,这时候yield,调度器会调度其他协程运行,可能调度几百几千个其他协程,最后再回到A协程进行recv,它的实时性有没有意义?
对于大量io,所有io一起看的话,单个io的实时性是没有意义的。