实现Coroutine

这段时间一直被老师压着在实验室干活,有段时间没有写博客了,抽个空记录下去年写的一个简单的协程的实现。

协程

老规矩,先上wiki定义。在我理解上,协程本质上就是用户态线程,出现的目的是用来缓解系统线程过多时带来的系统调用和切换开销,另外一种横向对比的技术就是callback,这两者之间的对比都在我之前的一篇博客中有过介绍。

现在有很多语言都内置了对协程的支持,比如go还有python。当然goroutine和我们常理解的coroutine还是有些不同的,这些不同就留给以后我再抽空来写一篇介绍吧,今天主要是介绍如何用c在linux下实现一个简洁的协程库。

特性

既然已经有这么多语言支持协程了,那其实我在一开始考虑实现的时候就有想过实现的参考对象。从复杂度上来讲,我没有考虑支持类似于goroutine那样的M:N的调度器,反倒是python早期用generator来实现coroutine的模型比较合适。但是c语言是跑在系统的栈上的,不是像python那样在堆上有PyFrameObject对象,所以我还需要自己处理协程栈。

既然选定了参考对象,那么就可以列出要实现的协程库的几点特性。

1 支持yield和resume来实现手工切换

2 在yield和resume中支持协程间变量的传递

3 合适的处理协程切换时栈的切换

协程切换的实现

c语言并不支持函数的挂起和恢复,那么在切换协程的时候我们就必须要手工保存上下文,便于在恢复的时候使用。那所谓的上下文其实就是函数使用的寄存器和栈上的所有变量。既然要操作系统的栈,c语言就不合适了,只能使用汇编,当然linux本身也提供了ucontex来实现上下文切换,当然还有setjmp和longjmp也可以做到这一点。

在实现的时候我使用了linux的ucontext,总共有四个函数,最常用的就是makecontext和swapcontext,函数顾名思义,makecontext是用来修改上下文,把上下文的入口改成某个函数,并且可以传递参数给这个函数。swapcontext实现上下文切换。我简单看了看这两个函数的实现,有点类似于linux kernel中进程切换的实现,但是这比kernel中的schedule函数的是实现简单多了。

协程间变量的传递

类似于python的generator,yield可以将某个变量传回给主程序。这里不同的协程只是用户空间看起来而已,对于系统,都是同一个系统线程,他们共享地址空间,所以对于不同协程间变量的共享和传递,我们可以简单的在将一个协程要传递的变量放在堆中的某个位置上,然后将这个位置告诉给要切换过去的协程即可。位置的话就用某个类成员变量即可。

栈的处理

其实协程栈的处理相对而言方式有很多种。在这里我是用的是copy stack,就是为每个协程在堆中中保存它的栈,在协程切换的时候将系统当前的栈拷贝到当前运行的协程中,然后将要切换过去的栈拷贝到系统栈上。

这样做的优点是简单,缺点是切换中拷贝带来的开销在某些应用中不能容忍。好在我实现的协程库要求每个协程有最大的栈大小,一般而言开销并不会太大。其他的实现方式还有segmented stack,按需分配,开销相对要小一些,但是实现起来也比较复杂,这种方式gcc是支持的。

其他

我还实现了一个简单的调度器,用来处理不同协程的调度。但是仅仅只是考虑本系统线程内的协程,没有做多线程上的调度。golang的调度器就有多个系统线程,用来处理所有goroutine的调度,还可以使用工作窃取算法来平衡不同系统线程的负载。

另外,对于网络编程而言,可以考虑hook系统调用,将fd设置为非阻塞状态,在read或者write可能阻塞的时候将对应的fd挂载到epoll上面去,然后返回调度器,根据epoll的返回的可读或可写fd来选取下一个协程来运行。

Reference

Coroutine