什么是协程
在单线程中实现多线程的编程模式。协程没有多线程的上下文切换消耗,适合IO密集型程序。可以用协程+多进程利用多核。我们用下面一段伪代码来解释什么是协程:
#include <stdio.h>
void ThreadA() {
printf("A0\n");
printf("A1\n");
}
void ThreadB() {
printf("B0\n");
printf("B1\n");
}
int main() {
while (true) {
ThreadA();
ThreadB();
}
return 0;
}
如果我们实现下面的输出,也就实现了协程:
A0
B0
A1
B1
……
对于这种输出,ThreadA和ThreadB就像是两个独立的线程在运行。那么,如何实现这种输出即如何实现协程呢?
利用语法技巧实现协程
#include <stdio.h>
void ThreadA(void) {
static int state = 0;
switch (state) {
case 0: goto LABEL0;
case 1: goto LABEL1;
}
LABEL0:
state = 1;
printf("A0\n");
return;
LABEL1:
state = 0;
printf("A1\n");
}
void ThreadB(void) {
static int state = 0;
switch (state) {
case 0: goto LABEL0;
case 1: goto LABEL1;
}
LABEL0:
state = 1;
printf("B0\n");
return;
LABEL1:
state = 0;
printf("B1\n");
}
int main() {
while (true) {
ThreadA();
ThreadB();
}
return 0;
}
state静态变量保存了函数上次调用的位置(可以理解为协程的“堆栈”),再利用c/c++的goto语言特性,我们实现了协程。
利用语法技巧实现的协程库
Protothreads - Lightweight, Stackless Threads in C 这是一个开源C协程库,有效代码不足100行,原理和上面讲的类似。
利用函数调用栈实现的协程库——libco
libo是利用函数调用栈特点实现的开源协程库。我们先来看下函数调用栈结构:
以下面函数调用举例:
void Func(int x, int y, int z)
{
int a, b, c;
return;
}
Func(10, 5, 2);
Func的汇编代码如下:
Func:
push %ebp # 将ebp压栈(保存函数调用者的栈基址,后面利用ebp来定位变量的地址)
mov %esp, %ebp # 将ebp指向栈顶esp(设置当前函数的栈基址)
sub esp, 12 # 分配栈空间 sizeof(a) + sizeof(b) + sizeof(c)
... # x = [ebp + 8], y = [ebp + 12], z = [ebp + 16]
# a = [ebp - 4] = [esp + 8], b = [ebp - 8] = [esp + 4], c = [ebp - 12] = [esp]
mov esp, ebp # Func收尾工作,esp和ebp开始指向函数调用者的栈帧
pop ebp
ret 12 # sizeof(x) + sizeof(y) + sizeof(z),返回到调用Func函数处继续执行
函数调用栈如下:
: :
| 2 | [ebp + 16] (3rd function argument)
| 5 | [ebp + 12] (2nd argument)
| 10 | [ebp + 8] (1st argument)
| RA | [ebp + 4] (return address) # 这里是协程关键,我们可以改变RA的值,从而让函数调用完后返回到协程指定的地址
| FP | [ebp] (old ebp value)
| | [ebp - 4] (1st local variable)
: :
: :
| | [ebp - X] (esp - the current stack pointer. The use of push / pop is valid now)
函数Func执行完后会返回调用处继续执行,libco的原理就是改变返回的地址RA,使函数执行完后跳转到指定的协程函数处运行。这部分是通过libco的coctx_swap函数实现的,其定义如下:
extern void coctx_swap( coctx_t ,coctx_t ) asm(“coctx_swap”);
coctx_swap是用汇编写的,不是标准的函数调用栈汇编,因为改变了函数调用的返回地址,其汇编实现如下:
先来看相关数据结构:
#define ESP 0
#define EIP 1
#define EAX 2
#define ECX 3
...
struct coctx_t
{
void *regs[ 8 ];
};
我们来分析coctx_swap(ctx1, ctx2),首先将当前的上下文环境保存到ctx1结构中(ctx1,ctx2中保存了要执行的函数):
leal 4(%esp), %eax # eax = old_esp + 4
movl 4(%esp), %esp # 将esp的值设为ctx1的地址(ctx1就是coctx_swap第一个参数的地址)
leal 32(%esp), %esp # esp = (char*)&ctx1 + 32
pushl %eax # ctx1->regs[EAX] = %eax
pushl %ebp # ctx1->regs[EBP] = %ebp
pushl %esi # ctx1->regs[ESI] = %esi
pushl %edi # ctx1->regs[EDI] = %edi
pushl %edx # ctx1->regs[EDX] = %edx
pushl %ecx # ctx1->regs[ECX] = %ecx
pushl %ebx # ctx1->regs[EBX] = %ebx
pushl -4(%eax) # ctx1->regs[EIP] = RA (将返回地址保存在ctx1的EIP中)
然后该函数将ctx2中保存的上下文恢复到寄存器中,并跳转到其函数地址处运行:
movl 4(%eax), %esp # 将esp的值设为ctx2的地址
popl %eax # %eax = ctx1->regs[EIP],即&pfn
popl %ebx # %ebx = ctx1->regs[EBP]
popl %ecx # %ecx = ctx1->regs[ECX]
popl %edx # %edx = ctx1->regs[EDX]
popl %edi # %edi = ctx1->regs[EDI]
popl %esi # %esi = ctx1->regs[ESI]
popl %ebp # %ebp = ctx1->regs[EBP]
popl %esp # %esp = ctx1->regs[ESP]
pushl %eax # RA = %eax = &pfn,此时esp已经指向了新的esp
xorl %eax, %eax # reset eax
ret
通过coctx_swap(ctx1, ctx2)就实现了跳转到ctx2中的函数上去执行。libco的使用见项目https://github.com/Tencent/libco 这里不再赘述。