用过python或者lua等脚本语言一定对协程印象深刻。这种把异步当同步写的做法很容易被大家接受,而其实协程的起源非常早,最早提出“协程”概念的 Melvin Conway 是为了解决cobol编译器问题,Conway 构建的这种协同工作机制,需要参与者“让出 (yield)”控制流时,记住自身状态,以便在控制流返回时能够从上次让出的位置恢复(resume)执行。本质是控制流的转换,但是因为命令式编程主流是自顶向下的设计,与协程理念是冲突的也导致协程之前只在一些小众语言实现了。但是随着C10K问题的出现,线程与进程开销无法支撑,linux率先利用epoll非阻塞来解决这个问题,基于事件异步控制流很容易出现callback hell,协程再次进入人们视野。我们先看一个类似协程的例子
def consumer():
r = ''
while True:
n = yield r
if not n:
return
time.sleep(1)
r = '200 OK'
def produce(c):
c.next()
n = 0
while n < 5:
n = n + 1
r = c.send(n)
dosome(r)
c.close()
相对于用锁来实现状态的保护,这段代码简直良心。
那协程如何实现 开销多大 以及协程本质是什么
协程切换的本质是寄存器的保存和恢复上次寄存器状态继续执行,协程拥有自己的栈空间,看起来和线程切换也没有什么区别 只是协程是用户在用户态自己实现 线程管理是内核帮忙管理对应用层透明。
我知道的几个协程开源库 libco,云风的coroutine ,还有神人写的用switch-case完成的Protothreads(可以看coolshell 的一个“蝇量级” C 语言协程库) 这几个都是c/c++实现的协程库,这里选择云风的来解释下怎么实现协程以及如何利用c语言的调度原语setjmp来实现
首先来看云风的API设计,云风的API设计参考了lua的用法
#ifndef C_COROUTINE_H
#define C_COROUTINE_H
#define COROUTINE_DEAD 0
#define COROUTINE_READY 1
#define COROUTINE_RUNNING 2
#define COROUTINE_SUSPEND 3
struct schedule;
typedef void (*coroutine_func)(struct schedule *, void *ud);
struct schedule * coroutine_open(void);
void coroutine_close(struct schedule *);
int coroutine_new(struct schedule *, coroutine_func, void *ud);
void coroutine_resume(struct schedule *, int id);
int coroutine_status(struct schedule *, int id);
int coroutine_running(struct schedule *);
void coroutine_yield(struct schedule *);
#endif
很好的c代码设计 不透明指针隐藏了结构体设计 下面来看下调度器和协程结构体设计
struct coroutine;
struct schedule {
char stack[STACK_SIZE];//共享栈空间
ucontext_t main;// 保存resume上下文
int nco;//协程数
int cap;//协程容量
int running;// 正在运行的协程id
struct coroutine **co;//协程数组
};
struct coroutine {
coroutine_func func;
void *ud;//私有参数
ucontext_t ctx;//运行func上下文
struct schedule * sch;//调度器
ptrdiff_t cap;//栈容量
ptrdiff_t size;//使用栈大小
int status;//协程状态
char *stack;//私有栈
};
云风的设计采用的是共享栈,也有一些库是采用私有栈,共享栈优势在于节省空间在调用层次不深情况下 开大量协程内存占用小 缺点是一旦调用层次深可能会大量的分配内存导致性能下降,利用了ucontext组件 具体用法可以参考这个
http://pubs.opengroup.org/onlinepubs/007908799/xsh/ucontext.h.html
这里只分析核心的三个函数 其他函数很好理解 coroutine_resume _save_stack 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) {
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 = &S->main;
S->running = id;
C->status = COROUTINE_RUNNING;
uintptr_t ptr = (uintptr_t)S;
makecontext(&C->ctx, (void (*)(void)) mainfunc, 2, (uint32_t)ptr, (uint32_t)(ptr>>32));
swapcontext(&S->main, &C->ctx);
break;
case COROUTINE_SUSPEND:
memcpy(S->stack + STACK_SIZE - C->size, C->stack, C->size);//拷贝协程的栈到共享栈
S->running = id;
C->status = COROUTINE_RUNNING;
swapcontext(&S->main, &C->ctx);
break;
default:
assert(0);
}
}
resume 思路是根据需要恢复运行的协程id 取出对应协程,通过协程状态看是初始化ucontext组件 保存运行栈还是拷贝私有协议栈 copy-out来切换栈
swapcontext(main, ctx) 保存resume的上下文 并且切换到ctx上下文执行
void
coroutine_yield(struct schedule * S) {
int id = S->running;
assert(id >= 0);
struct coroutine * C = S->co[id];
assert((char *)&C > S->stack);
_save_stack(C,S->stack + STACK_SIZE);
C->status = COROUTINE_SUSPEND;
S->running = -1;
swapcontext(&C->ctx , &S->main);
}
实现了协程悬挂 思路是找到当前运行的协程保存私有栈信息到ctx的私有栈 设置协程状态为悬挂状态 切换到resume执行
这个函数重点是_save_stack
tatic void
_save_stack(struct coroutine *C, char *top) {
char dummy = 0;
assert(top - &dummy <= STACK_SIZE);
if (C->cap < top - &dummy) {
free(C->stack);
C->cap = top-&dummy;
C->stack = malloc(C->cap);
}
C->size = top - &dummy;
memcpy(C->stack, &dummy, C->size);
}
理解这个函数首先要理解栈帧 top是栈顶 从高地址向低地址扩,dummy是哑节点,地址是栈信息的终点,拷贝栈信息到协程私有栈上就完成了copy-in
假如没有ucontext呢 ,我们可以利用c语言的jmpbuf来实现
jmpbuf保存的就是一些寄存器值 不同平台不一样 下面是linux内核的定义如下
/*
* arch/um/include/sysdep-i386/archsetjmp.h
*/
#ifndef _KLIBC_ARCHSETJMP_H
#define _KLIBC_ARCHSETJMP_H
struct __jmp_buf {
unsigned int __ebx;
unsigned int __esp;
unsigned int __ebp;
unsigned int __esi;
unsigned int __edi;
unsigned int __eip;
};
typedef struct __jmp_buf jmp_buf[1];
#define JB_IP __eip
#define JB_SP __esp
#endif /* _SETJMP_H */
主要的寄存器 esp,ebp,eip 这里有一个c语言技巧 就是把结构体定义为数组 这样可以当指针用 在堆上直接分配内存
esp 存储栈顶指针 ebp存储当前函数运行状态基地址 eip存储即将运行的指令 知道这几个我们就可以模拟ucontext 比如makecontext 就是设置栈顶指针,保存运行函数地址 实现如下
mark 下次写