协程原理
协程的本质都是通过修改 ESP 和 EIP 指针来实现的。其理论上还是单线程在运行,要想实现真正的并发,其实是需要多个CPU才能的。并发是并行的充分条件,并发是在程序级别上实现的,而并行是在机器级别上实现的,要想实现并行,程序上必须以并发实现,但是程序上实现了并发并不代表是真正的并行,当只有一个CPU的时候还是属于单线程。
程序在CPU上运行时依赖2个条件:
堆栈指针 ESP 寄存器,指向当前指令需要的数据
指令指针 EIP 寄存器,指向当前需要运行的指令
这两个寄存器指针的改变可以修改当前需要加载到 CPU 运行的指令和数据,当某个操作陷入到耗时的等待中时,通过修改这两个指针即可让出CPU,交给其他的任务去使用,每个任务都必须主动的让出CPU,然后等待下一次的调度来继续未完成的任务,这样就可以最大程度的利用CPU,当一个任务等待过程非常短的时候,就出现了多个任务并行运行的效果,也就是协程。
目前常见的协程库有云风的coroutine , libtask 库,腾讯的 libco ,以下以最简单的 coroutine 库讲解一下。
云风的协程库实现
C语言实现协程主要依赖于一组 glibc 库里的上下文操作函数:
getcontext() : 获取当前context
setcontext() : 切换到指定context
makecontext() : 设置 函数指针 和 堆栈 到对应context保存的sp和pc寄存器中,调用之前要先调用 getcontext()
swapcontext() : 保存当前context,并且切换到指定context
可以看下 makecontext 的原理
void makecontext(ucontext_t *uc, void (*fn)(void), int argc, ...)
{
int i, *sp;
va_list arg;
// 将函数参数陆续设置到r0, r1, r2 .. 等参数寄存器中
sp = (int*)uc->uc_stack.ss_sp + uc->uc_stack.ss_size / 4;
va_start(arg, argc);
for(i=0; i<4 && i<argc; i++)
uc->uc_mcontext.gregs[i] = va_arg(arg, uint);
va_end(arg);
// 设置堆栈指针到sp寄存器
uc->uc_mcontext.gregs[13] = (uint)sp;
// 设置函数指针到lr寄存器,切换时会设置到pc寄存器中进行跳转到fn
uc->uc_mcontext.gregs[14] = (uint)fn;
}
因此我们可以知道一个协程就是一组包含了上下文运行环境和一个私有栈的结构。在设计时一个协程需要有以下数据成员
struct coroutine {
coroutine_func func; //协程运行主体函数
void *ud; //func 的参数
ucontext_t ctx; //该协程的上下文信息
struct schedule * sch; //对应的调度器
ptrdiff_t cap; //协程