协程又被称为微线程,不过其实这样的称呼无形中为理解协程增加了一点阻碍。协程本质上是在一个线程里面,因此不管协程数量多少,它们都是串行运行的,也就是说不存在同一时刻,属于同一个线程的不同协程同时在运行。因此它本身避免了所有多线程编程可能导致的同步问题。
协程的行为有点像函数调用,它和函数调用的不同在于,对于函数调用来说,假如A函数调用B函数,则必须等待B函数执行完毕之后程序运行流程才会重新走回A,但是对于协程来说,如果在协程A中切到协程B,协程B可以选择某个点重新回到A的执行流,同时允许在某个时刻重新从A回到B之前回到A的那个点,这在函数中是不可能实现的。因为函数只能一走到底。用knuth的话来说:
子程序就是协程的一种特例
既然允许协程中途切换,以及后期重新从切换点进入继续执行,说明必须有数据结构保存每个协程的上下文信息。这就是ucontext_t,而linux中包含以下几个系统函数对ucontext_t进行初始化,设置,以及基于ucontext_t进行协程间的切换:
- int getcontext(ucontext_t *);
- int setcontext(const ucontext_t *);
- void makecontext(ucontext_t , (void )(), int, …);
- int swapcontext(ucontext_t , const ucontext_t );
下面根据云风的精简协程库来说明如何用这些函数和ucontext_t来实现一个协程池。
coroutine的实现分析
coroutine里面主要是由协程管理结构schedule和以下几个核心函数组成 :
- coroutine_resume
- coroutine_yield
coroutine由用户协程和一个管理作用的协程组成。每次协程切换时,用户协程必须先切换到管理协程,然后管理协程再负责切换到其他的用户协程,不能直接从用户协程切换到其他用户协程。从以上两个函数来说,coroutine_resume就是从管理协程切换到用户协程的入口,coroutine_yield是从用户协程切换到管理协程的入口。
下面着重分析一下这两个函数:
void
coroutine_resume(struct schedule * S, int id) {
assert(S->running == -1);
assert(id >=0 && id < S->cap);
struct coroutine *C = S->co[id];
if (C == NULL)
return;
int status = C->status;
switch(status) {
//第一次被切换到的用户协程处于COROUTINE_READY
case COROUTINE_READY:
getcontext(&C->ctx);
C->ctx.uc_stack.ss_sp = S->stack;
C->ctx.uc_stack.ss_size = STACK_SIZE;
C->ctx.uc_link = &<