协程概念

协程是计算机程序的一类组件,允许执行被挂起和恢复。

协程与函数比较

  • 函数只有一个入口点和出口点;协程有多个
  • 函数可以调用其它函数,调用者等待被调用者结束后继续执行 ,生命周期遵循后进先出;协程的生命周期有他们的使用需要来决定
  • 函数一次性返回全部结果;协程可以每次返回部分结果,这种协程称为生成器

协程有多个入口点,执行过程过

协程与线程比较

  • 线程是抢占式多任务处理;协程是协作式多任务处理
  • 线程能提供并行性,协程只能并发

协程与回调函数比较

  • 回调函数异步时编码复杂,可读性差;协程以顺序方式组织异步代码,可读性更好

协程实现

函数的机器级表示 里提到,函数调用时将调用点保存在栈里,函数返回从栈中取出调用点地址,直接跳转回调用点。同时函数的局部变量也保存在栈中。

协程分为有栈协程和无栈协程:

  • 有栈协程通过类似函数,为每个协程分配栈空间保存上下文,切换时保存当前栈的完整状态,恢复时恢复另一个协程的栈状态。
  • 无栈协程:所有协程在同一个线程栈上运行。挂起时,通过状态机记录状态,将局部变量等数据(延续)保存在堆上,并返回。恢复时,重新调用函数并从保存的状态开始执行.
有栈协程无栈协程
优点任意函数挂起和恢复开销小,不需要栈空间
缺点需要为每个协程分配栈空间,消耗大只能在语言定义的特定点挂起,比如 await 处

有栈协程

有栈协程可以实现为每个协程独立一个栈,或者所有协程共享一个栈,下面实现一个简单的有栈协程

  1. 定义协程对象,协程栈大小为 64 KB

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    typedef struct co co_t;
    
    typedef void (*co_entry_t)();
    
    #define CO_STACK_SIZE (64 * 1024)  // 64KB
    
    struct co_ctx {
        void* rsp;
        void* rbx;
        void* rbp;
        void* r12;
        void* r13;
        void* r14;
        void* r15;
    };
    
    struct co {
        co_entry_t entry;
        struct co_ctx ctx;
        char stack[CO_STACK_SIZE];
    };
  2. 创建一个协程

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    co_t* co_create(co_entry_t entry) {
        co_t* co = malloc(sizeof(co_t));
        if (!co) return NULL;
    
        co->entry = entry;
    
        // 协程的栈顶
        char* stack_top = co->stack + CO_STACK_SIZE;
        stack_top = (char*)((uintptr_t)stack_top & ~0xF);
    
        // 将 entry 地址放到栈中
        stack_top -= sizeof(void*);
        *(void**)stack_top = (void*)entry;
    
        co->ctx.rsp = stack_top;
        return co;
    }

    内存视图

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
     -----| stack_bottom
    |entry|
     -----| stack_top
    |     |
    |     |
    |     |
    |     |
    |-----|
    |     |
    |co_t |
    |     |
    |-----|
  3. 协程上下文切换

    1
    
    extern void co_ctx_swap(struct co_ctx* from, struct co_ctx* to);
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
    # co_ctx_swap.S
    .text
    .globl co_ctx_swap
    .type co_ctx_swap, @function
    
    co_ctx_swap:
        movq %rsp, (%rdi)
        movq %rbx, 8(%rdi)
        movq %rbp, 16(%rdi)
        movq %r12, 24(%rdi)
        movq %r13, 32(%rdi)
        movq %r14, 40(%rdi)
        movq %r15, 48(%rdi)
    
        movq 48(%rsi), %r15
        movq 40(%rsi), %r14
        movq 32(%rsi), %r13
        movq 24(%rsi), %r12
        movq 16(%rsi), %rbp
        movq 8(%rsi), %rbx
        movq (%rsi), %rsp
    
        ret
    
    .size co_ctx_swap, .-co_ctx_swap

    首先将当前上下文保存在第一个参数,然后将切换的上下文赋值给寄存器,最后 ret 指令将 entry 地址弹出并将 PC 寄存器指向 entry

  4. 协程运行

    1
    2
    3
    4
    5
    6
    7
    8
    
    static co_t* cur_co = NULL;
    static struct co_ctx main_ctx;
    
    void co_resume(co_t* co) {
        if (!co) return;
        cur_co = co;
        co_ctx_swap(&main_ctx, &co->ctx);
    }

    当前只实现一个协程,因此简单的用 cur_co 记录

  5. 协程让出

    1
    2
    3
    
    void co_yield(void) {
        co_ctx_swap(&cur_co->ctx, &main_ctx);
    }
  6. 协程销毁

    1
    2
    3
    4
    
    void co_destroy(co_t* co)
    {
        free(co);
    }
  7. 测试程序

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    
    
    #include "coroutine.h"
    
    #include <stdio.h>
    
    void test(void)
    {
        int a = 10;
        printf("test 1... a = %d\n", a);
        co_yield();
    
        a++;
        printf("test 2... a = %d\n", a);
        co_yield();
    
        a++;
        printf("test 3... a = %d\n", a);
        co_yield();
    }
    
    int main(int argc, char *argv[])
    {
    
        co_t* co = co_create(test);
    
        co_resume(co);
    
        printf("main 1....\n");
    
        co_resume(co);
    
        printf("main 2 ...\n");
    
        co_resume(co);
    
        printf("main end\n");
    
        co_destroy(co);
    
        return 0;
    }
  8. 运行结果

    1
    2
    3
    4
    5
    6
    
    test 1... a = 10
    main 1....
    test 2... a = 11
    main 2 ...
    test 3... a = 12
    main end

无栈协程

无栈协程使用状态机进行跳转,使用堆内存保存协程状态和局部变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

typedef struct co co_t;
typedef void (*co_entry_t)(co_t* co);

struct co {
    co_entry_t entry;
    int state;
    int val;
};

co_t* co_create(co_entry_t entry) {
    co_t* co = malloc(sizeof(co_t));
    if (co == NULL) {
        return NULL;
    }
    co->entry = entry;
    co->state = 0;
    co->val = 0;
    return co;
}

void co_resume(co_t* co) {
    co->entry(co);
}

void co_destroy(co_t* co) {
    free(co);
}

void test(co_t* co) {
    switch (co->state) {
    case 0:
        co->val = 10;
        printf("test 1... a = %d\n", co->val);
        co->state = 1;
        return;
    case 1:
        co->val++;
        printf("test 2... a = %d\n", co->val);
        co->state = 2;
        return;
    case 2:
        co->val++;
        printf("test 3... a = %d\n", co->val);
        return;
    }
}

int main() {

    co_t* co = co_create(test);

    co_resume(co);

    printf("main 1....\n");

    co_resume(co);

    printf("main 2 ...\n");

    co_resume(co);

    printf("main end\n");

    co_destroy(co);

    return 0;
}
1
2
3
4
5
6
test 1... a = 10
main 1....
test 2... a = 11
main 2 ...
test 3... a = 12
main end

上面的 test 协程实际上由编译器处理