协程及其实现

什么是协程

在单线程中实现多线程的编程模式。协程没有多线程的上下文切换消耗,适合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 这里不再赘述。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值