cloudwu/coroutine 源码分析

cloudwu/coroutine 源码分析

1 与其它协程库使用对比

这个 C 协程库是云风(cloudwu) 写的,其接口风格与 Lua 协程类似,并且都是非对称 stackful 协程。这个是源代码中的示例:

#include "coroutine.h"
#include <stdio.h>

struct args
{
    int n;
};

static void
foo(struct schedule *S, void *ud)
{
    struct args *arg = ud;
    int start = arg->n;
    int i;
    for (i = 0; i < 5; i++)
    {
        printf("coroutine %d : %d
", coroutine_running(S), start + i);
        coroutine_yield(S);
    }
}

static void
test(struct schedule *S)
{
    struct args arg1 = {0};
    struct args arg2 = {100};

    int co1 = coroutine_new(S, foo, &arg1);
    int co2 = coroutine_new(S, foo, &arg2);
    printf("main start
");
    while (coroutine_status(S, co1) && coroutine_status(S, co2))
    {
        coroutine_resume(S, co1);
        coroutine_resume(S, co2);
    }
    printf("main end
");
}

int main()
{
    struct schedule *S = coroutine_open();
    test(S);
    coroutine_close(S);

    return 0;
}

这段代码输出:

main start
coroutine 0 : 0
coroutine 1 : 100
coroutine 0 : 1
coroutine 1 : 101
coroutine 0 : 2
coroutine 1 : 102
coroutine 0 : 3
coroutine 1 : 103
coroutine 0 : 4
coroutine 1 : 104
main end

与其等价的 Lua 代码为:

local function foo(args)
    local start = args.n
    for i = 0, 4 do
        print(string.format("coroutine %s : %d", coroutine.running(), start + i))
        coroutine.yield()
    end
end

local function test()
    local arg1 = {n = 0}
    local arg2 = {n = 100}

    local co1 = coroutine.create(foo)
    local co2 = coroutine.create(foo)
    print("main start")
    coroutine.resume(co1, arg1)
    coroutine.resume(co2, arg2)
    while coroutine.status(co1) ~= "dead" and coroutine.status(co2) ~= "dead" do
        coroutine.resume(co1)
        coroutine.resume(co2)
    end
    print("main end")
end

test()

这段代码输出:

main start
coroutine thread: 000001D62BD6B8A8 : 0
coroutine thread: 000001D62BD6BA68 : 100
coroutine thread: 000001D62BD6B8A8 : 1
coroutine thread: 000001D62BD6BA68 : 101
coroutine thread: 000001D62BD6B8A8 : 2
coroutine thread: 000001D62BD6BA68 : 102
coroutine thread: 000001D62BD6B8A8 : 3
coroutine thread: 000001D62BD6BA68 : 103
coroutine thread: 000001D62BD6B8A8 : 4
coroutine thread: 000001D62BD6BA68 : 104
main end

与其等价的 C++ 20 代码为:

#include <coroutine>
#include <functional>
#include <stdio.h>

struct coroutine_running
{
    bool await_ready() { return false; }
    bool await_suspend(std::coroutine_handle<> h) {
        _addr = h.address();
        return false;
    }
    void* await_resume() {
        return _addr;
    }
    void* _addr;
};

struct return_object : std::coroutine_handle<>
{
    struct promise_type
    {
        return_object get_return_object() {
            return std::coroutine_handle<promise_type>::from_promise(*this);
        }
        std::suspend_always initial_suspend() noexcept { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() {}
    };

    return_object(std::coroutine_handle<promise_type> h) : std::coroutine_handle<>(h){}
};

struct args
{
    int n;
};

return_object foo(args* arg)
{
    int start = arg->n;
    for (int i = 0; i < 5; i++)
    {
        printf("coroutine %p : %d
", co_await coroutine_running{}, start + i);
        co_await std::suspend_always{};
    }
}

int main()
{
    args arg1 = { 0 };
    args arg2 = { 100 };
    auto co1 = foo(&arg1);
    auto co2 = foo(&arg2);
    printf("main start
");
    while (!co1.done() && !co2.done())
    {
        co1.resume();
        co2.resume();
    }
    co1.destroy();
    co2.destroy();
    printf("main end
");
}

这段代码输出

main start
coroutine 0x607000000020 : 0
coroutine 0x607000000090 : 100
coroutine 0x607000000020 : 1
coroutine 0x607000000090 : 101
coroutine 0x607000000020 : 2
coroutine 0x607000000090 : 102
coroutine 0x607000000020 : 3
coroutine 0x607000000090 : 103
coroutine 0x607000000020 : 4
coroutine 0x607000000090 : 104
main end

对比三段代码,可以看到 C 版本比 Lua 版本多了 coroutine_open 和 coroutine_close,C 版本需要保存一个全局状态 S。Lua 版本无法在创建协程的时候指定参数,必须在之后的 resume 把参数传递给 foo。C++ 20 版本需要手写第 5~35 行的框架代码才能达到同样的效果。抛开这三段代码的差异,能看到协程库的共性:创建协程(create)、恢复协程(resume)、让出协程(yield)、销毁协程(destroy)。cloudwu/coroutine 的源代码行数非常少,所以以此分析一个协程库如何实现这些机制。

在分析代码之前需要明确协程的一些基本概念,coroutine 的 co 并不是 concurrency,而是 cooperative。协程就是能协作执行的例程(routine)。函数(function)也是例程,但不是协程。协程和函数是类似概念。协程和线程不类似,和线程类似的概念是纤程(Fiber)。关于协程和纤程的区别可以看 N4024。协程按能不能在嵌套栈帧中挂起(suspend),分为:stackless 协程 和 stackful 协程。stackless 协程只能在最顶层栈帧挂起,stackful 协程能在任意栈帧挂起。C++ 20 协程是 stackless 协程。所以在上面的代码中,如果 foo 再调用一个函数 bar,bar 就无法使用 co_await。但是 C 版本和 Lua 版本的协程可以这样,所以它们是 stackful。如果 a resume b,则称 a 为 b 的 resumer。协程还可以按控制流(control flow)切换方式分为:对称(symmetric)协程和非对称(asymmetric)协程。非对称协程通过 yield 切换到其 resumer,不需要指定切换到哪个协程。对称协程每次切换都需要指定切换到哪个协程,它可以切换到任意协程。C 版本和 Lua 版本的协程都只支持非对称协程,但是可以通过非对称协程实现对称协程。C++ 20 协程两者都支持。

源代码只有两个文件 coroutine.h 和 coroutine.c。

2 coroutine.h

先看 coroutine.h,有 4 个宏定义

Name Value
COROUTINE_DEAD 0
COROUTINE_READY 1
COROUTINE_RUNNING 2
COROUTINE_SUSPEND 3

显然,这个 4 个宏定义了协程的四种状态,协程创建完还没执行是 COROUTINE_READY,正在执行是COROUTINE_RUNNING,被挂起是 COROUTINE_SUSPEND,已经结束是 COROUTINE_DEAD。为方面阅读,下面直接用 Ready、Running、Suspend、Dead 指代。

一个 schedule 结构体,保存了用来做协程调度的信息,是一个协程组的全局状态。注意协程并没有调度器,也不需要像进程和线程那样的调度算法。每次协程的切换,切换到哪个协程都是固定的。所有属于同一个 schedule 的协程可以视为一个协程组。schedule 拥有这个协程组的信息。

一个函数指针定义 coroutine_func,这个函数表示协程的执行内容,也就是协程执行的时候会调用这个函数。该函数有一个参数 ud,ud 是 user data 的缩写,user 指代调用接口的程序员,user data 被用来传递数据。Lua 的 userdata 也用于 C 和 Lua 之间数据传递。

两个针对全局状态 S 的操作:coroutine_open 和 coroutine_close

五个针对单个协程的操作:coroutine_new、coroutine_resume、coroutine_status、coroutine_running、coroutine_yield。其中 coroutine_new 也有一个 ud 和 coroutine_func 的参数对应。根据这些状态和操作可以画出协程的状态图。

stateDiagram
[*] –> Ready : coroutine_new
Ready –> Running : coroutine_resume
Running –> Suspend : coroutine_yield
Suspend –> Running : coroutine_resume
Running –> Dead : normal finish / coroutine_close
Dead –> [*]

hmoban主题是根据ripro二开的主题,极致后台体验,无插件,集成会员系统
自学咖网 » cloudwu/coroutine 源码分析