cloudwu/coroutine 原始碼分析

2022-06-06 21:01:06

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\n", 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\n");
    while (coroutine_status(S, co1) && coroutine_status(S, co2))
    {
        coroutine_resume(S, co1);
        coroutine_resume(S, co2);
    }
    printf("main end\n");
}

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\n", 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\n");
    while (!co1.done() && !co2.done())
    {
        co1.resume();
        co2.resume();
    }
    co1.destroy();
    co2.destroy();
    printf("main end\n");
}

這段程式碼輸出

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 --> [*]

協程從 Running 轉變為 Dead,有兩種途徑,一是正常結束返回,二是呼叫 coroutine_close 關閉所有協程。兩者都會銷燬協程。協程被銷燬之後,其儲存的狀態也不存在了,只不過用 Dead 表示其不存在。觀察這個狀態圖,可以和 Lua 的協程狀態進行類比,發現 Lua 協程還具有 normal 狀態,也就是 main resume a, a resume b,a 是 normal 狀態。經過測試,C 版本協程庫無法巢狀 resume,a 不能 resume b。resumer 只能是 main routine。也就沒有 normal 狀態。

3 coroutine.c

在這個檔案裡包含了 ucontext.h,說明這個庫使用 ucontext 進行 context 切換。檢視 ucontext_t 的定義,可以看到它儲存了 CPU 的暫存器值和其它狀態資訊,包括通用暫存器 rax、rbx、rcx、rdx ...,用於浮點數計算的XMM暫存器,中斷遮蔽字 sigmask 等。

STACK_SIZE 定義了所有協程棧的容量為 1MB,DEFAULT_COROUTINE 定義了初始的協程數量為 16。注意初始的協程數量為 16,並不代表建立了 16 個協程,只是分配了大小為 16 的協程指標陣列。

schedule 結構體裡的 statck 是大小為 STACK_SIZE 的位元組陣列。表示協程組所有協程的工作棧,這個棧是所有協程共用的,它是固定分配的,不會動態擴容。當任意一個協程被恢復,這個協程會使用這個工作棧執行程式碼,分配棧變數。main 表示 main routine 的 ucontext。nco 表示當前存在的協程數量。cap 是 capacity 的縮寫,表示協程指標陣列的容量,初始大小是 DEFAULT_COROUTINE。running 表示當前正在執行的協程 id。co 是協程指標的指標,也就是一個協程指標陣列。為什麼使用協程指標陣列,而不直接使用協程陣列?因為擴容的時候效率更高。co 擴容的時候,如果使用協程指標陣列,挪動的每個格子大小為 sizeof(struct coroutine *)。反之,直接使用協程陣列,挪動的每個格子大小為 sizeof(coroutine)。哪個效率高,一目瞭然。

coroutine 結構體儲存了每個協程的狀態資訊。func 表示協程的執行內容,由 coroutine_new 的 func 引數指定。ud 表示 func 的引數,由 coroutine_new 的 ud 引數指定。ctx 表示該協程的 ucontext。sch 指向全域性狀態 S,也就是每個協程可以反向查詢到其屬於哪個協程組。cap 是該協程的私有棧容量,size 是實際佔用大小。status 表示該協程的狀態,也就是前面提到的四種狀態,但是 status 永遠不會被置為 Dead。因為一個協程銷燬之後,就會在 S 中的協程指標陣列中對應格子置為 NULL。status 沒有機會被置為 Dead。stack 表示該協程的私有棧,是動態擴容的。

共用棧是所有協程的工作棧,同一時間有且只有協程正在執行,所有隻需要一個工作棧就行了。每個協程都有自己的棧資料,因此每個協程都需要自己私有棧。為什麼不直接用私有棧作為工作棧,這樣還省去了共用棧和私有棧之間資料複製?因為這裡採取的策略是用時間換取空間。假設沒有共用棧,所有協程使用自己的私有棧作為工作棧。要保證同協程棧變數地址一致性,也就是一個區域性變數在協程切出去和切回來之後的地址是相同的,工作棧無法動態擴容。因為一旦擴容,無法保證這個一致性,user code 可能會出問題。所以工作棧無法動態擴容,所有私有棧大小都為 STACK_SIZE。假設有 1024 個協程都處於 Ready 狀態,即使還沒執行,也佔用了 1 GB 的記憶體。共用棧保證了同協程棧變數地址一致性,但是無法保證異協程棧變數地址一致性。例如,協程 a 存取協程 b 的一個棧變數地址,存取的是共用棧的地址,user code 無法正常工作。通常來說這種使用場景非常少,也可以將存取棧變數地址改為存取堆變數地址來規避這個問題。

下面從標頭檔案的介面函數入手,逐個進行分析

3.1 coroutine_open

該函數為 S 分配記憶體,初始化其每個成員,為協程指標陣列 co 預留 DEFAULT_COROUTINE 個格子,並全部置為 NULL。如果仔細觀察,可以發現 stack 並沒有進行 memset 操作,是忘記寫了嗎?其實是故意為之。因為棧記憶體的初始化應該由 user code 承擔,這裡沒必要進行初始化。main 也沒有進行初始化,實際上所有 ucontext_t 結構體都不需要初始化,它們在被使用之前都應該使用 getcontext 賦值。

3.2 coroutine_close

該函數銷燬所有協程,然後銷燬全域性狀態 S。這裡直接遍歷了所有的格子,因為現存的協程可能並不是正好佔據第 0 ~ nco - 1 個格子。

3.3 coroutine_new

該函數分配一個 coroutine 結構體並執行初始化。可以看到,此時協程的狀態被置為 Ready。協程的私有棧沒有被分配,等到協程真正要被執行的時候再分配。然後把協程指標儲存到 S 中,但是 S 的 co 格子數量可能不夠,需要動態擴容。這裡的擴容策略是係數為 2 的線性增長。這裡擴容使用了 realloc 函數,這個函數比使用 malloc + free 效率更高。如果格子數量足夠就從第 nco 個格子開始查詢空位。如果達到 cap - 1,就回繞從第 0 個格子繼續查詢。也就是進行迴圈查詢,能夠最大化利用 co 的所有空格。從第 nco 個格子開始查詢具有一定的效率優勢,例如假設現存 10 個協程,也沒有協程被銷燬,在分配第 11 個協程時,能夠直接找到第 10 個格子,它正好是空格子。但是,一旦協程被隨機銷燬,這種優勢會消失,佔用的格子會被隨機分佈,找第 nco 個格子和隨機選擇一個格子沒什麼區別。assert(0) 是一個執行期斷言,如果走到這裡,說明 S 的資料被破壞了,直接執行 abort() 終止程式。

3.4 coroutine_resume

該函數掛起當前的函數,恢復指定的目標協程。assert(S->running == -1); 說明呼叫者不能是屬於 S 協程組的協程。因為如果呼叫者是協程,runnning 不可能為 -1,程式會直接退出。assert(id >=0 && id < S->cap); 校驗 id 是否合法。接下來根據目標協程的狀態進行操作。可以看到只能 resume 一個狀態為 Ready 或 Suspend 的協程。

可以看到,恢復一個 Ready 協程,使用到了三個 UNIX 函數,分別為 getcontext、makecontext、swapcontext。實際上這組 UNIX 函數有四個,只不過只用了這三個。

#include <ucontext.h>

int getcontext(ucontext_t *ucp);
int setcontext(const ucontext_t *ucp);

void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...);
int swapcontext(ucontext_t *restrict oucp, const ucontext_t *restrict ucp);

getcontext 儲存當前的 context 到 ucp。setcontext 用 ucp 設定當前的 context,也就是啟用 ucp。這個函數一旦呼叫成功,就不會返回。這種呼叫成功不返回的函數,與 longjmp 類似,它也是這樣。實際上,使用者態 context 切換機制經歷了三個發展階段。第一階段是 setjmp/longjmp。第二階段是 sigsetjmp/siglongjmp,它能夠控制中斷遮蔽字。第三個階段是 getcontext/setcontext,它能夠提供更加完整的控制。

makecontext 可以修改一個 context,它可以修改 ucp 的裡面的一些暫存器和狀態,使得 ucp 被啟用後會呼叫 func(...) 函數。argc 表示後面的引數個數,後面可以傳遞任意個數的引數。但是為了可移植性,必須都是 int 型別。程式碼裡面使用的是 uint32_t 與 int 相符。Linux x86_64 的資料模型是 LP64,也就是 long 和 pointer 都是64位元。因此,如果要傳遞指標 S,則需要將 S 拆成兩個 32 位的 uintptr_t。當然這段程式碼也支援 Linux x86,高 32 位都是 0。在呼叫 makecontext 之前必須設定 ucp->uc_stack 和 ucp->uc_link。ucp->uc_stack 表示 ucp 啟用之後,func 函數在哪個棧上執行。因此需要對 uc_stack.ss_sp 和 uc_stack.ss_size 賦值。程式碼裡面直接設定了工作棧 S->stack,和其大小 STACK_SIZE。這裡需要注意的是 uc_stack.ss_sp 中的 sp 並不像 rsp 暫存器那樣表示 stack pointer,指向棧頂。x86 架構的棧都是從高地址向低地址擴充套件。如果 ss_sp 表示 stack pointer 棧頂,則應該賦值為 S->stack + STACK_SIZE。實際上,uc_stack.ss_sp 的 sp 表示 starting pointer,表示工作棧的起始地址,也就是 S->stack。當 ucp 被啟用後,會根據 CPU 架構自動設定 rsp,所以不必考慮這些。ucp->uc_link 表示 func 函數執行完成之後啟用哪個 context。顯然,協程執行完成之後,應該繼續執行其 resumer,也就是 main routine。注意,程式碼裡的 uc_link 賦值時,S->main 還沒有任何有效內容。但是 uc_link 只需要儲存 ucontext_t 的指標,所以可以這樣做。S->running 賦值為 id。這裡協程還沒有真正執行,context 也沒有切換,只是方便傳參,mainfunc 可以通過 S 拿到 id。

swapcontext 並不是交換其兩個引數 oucp 和 ucp。而是 oucp、當前 context、ucp 三者之間進行交換。 流程如下圖所示:

flowchart RL cucp(當前 context) ucp -- 2 --> cucp -- 1 --> oucp

必須按順序先將當前context 儲存到 oucp,再啟用 ucp。不然 S->main 的內容還是無效的,就被切換到了 mainfunc。這裡使用 swapcontext,而不是分兩次呼叫 getcontext 和 setcontext。假設使用 getcontext + setcontext,context 切換出去,後面再切回來的時候,會回到 getcontext 剛剛執行之後,又會再次執行 setcontext,這顯然不行。使用 swapcontext 可以規避這個問題,當 context 切出去再切回來時,會正好回到 swapcontext 剛剛執行完畢。

為什麼切換到 mainfunc,而不是直接切換到 C->func ?因為當 C->func 執行完成之後,需要銷燬協程物件 C,也需要修改 S 的資訊。所以使用 mainfunc 對 C->func 進行了包裝。可以看到 mainfunc 執行了 C->func 之後確實進行了一些清理操作。所以協程執行完畢之後不必手動進行 destroy 操作,會自動釋放記憶體。如果想在 user code 裡面提前銷燬被掛起的協程怎麼辦?作者預留了 _co_delete 介面,它沒有出現在標頭檔案中,但也沒有用 static 修飾。也就是說,可以自己在 user code 寫一遍 _co_delete 的宣告,再手動清理。

恢復一個 Suspend 協程則相對簡單得多。C->ctx 早已儲存有效的資訊。不需要再次設定,也不需要呼叫 makecontext。這是需要 restore 協程私有棧的內容到工作棧 S->stack。由於私有棧僅儲存 user code 中已經分配的佔記憶體,所以只複製 C->size 大小的記憶體。這裡預設棧使用從高地址向低地址的擴充套件方式,也就是該程式碼只適用於這種棧擴充套件方式的架構,如 x86 架構。私有棧內容複製到共用棧的地址對齊如下圖所示,棧底的 S->stack + STACK_SIZE 和 C->stack + size 都是實際佔用記憶體的下一個地址。

                                         S->stack       C->stack
High Addr    S->stack + STACK_SIZE ---> +-------+       +-------+ <-- C->stack + size
    |                                   |       |       |       |
    |                                   |       |       |       |
    |        S->stack + STACK_SIZE      |       |       |       |
    |              - C->size       ---> |-------|<----- +-------+ <-- C->stack
    |                                   |       |
    V                                   |       |
Low Addr                  S->stack ---> +-------+

3.5 coroutine_yield

執行 coroutine_yield 會掛起當前協程,並且切換到其 resumer 繼續執行。如果簡單地使用 swapcontext,會出現問題。如協程 a 掛起,main routine 再恢復協程 b 執行一些操作,b 再掛起,main routine 再恢復 a。這時 a 的棧變數可能已經被 b 修改了。這是因為 a 和 b 共用一個工作棧。所以在 swapcontext 之前需要將當前協程的工作棧內容 store 到其私有棧。在呼叫 _save_stack 函數之前有一個斷言,assert((char *)&C > S->stack); 這個斷言只能用來校驗 &C 是否超過 S->stack 的下界。雖然 C 是一個指標,但是也是一個棧變數,其地址和當前工作棧的棧頂 S->stack 進行比較。如果 &C > S->stack 則表示 &C 的地址沒有超過下界。_save_stack 裡面還有 assert(top - &dummy <= STACK_SIZE); ,這個斷言作用也是如此。為什麼只檢測下界而不檢測上界?因為這裡預設棧擴充套件方式為從高地址向低地址擴充套件。所以只比較棧頂 S->stack。程式碼裡將 S->stack + STACK_SIZE 命名為 top,實際為棧底,但地址更高。

_save_stack 的 dummy 非常巧妙,其地址與棧底 top 之差就是從 dummy 到 top 之間的棧大小,棧頂就是 &dummy。假設在 dummy 之後又有宣告了區域性變數 x,這個區域性變數並沒有儲存下來。因此,在 _save_stack 和 swapcontext 之間宣告的任何區域性變數都不應該在 swapcontext 之後被使用。顯然,這裡的程式碼符合這個規範,不會出現問題。私有棧的記憶體也在 _save_stack 中分配。這裡採取的分配策略是按需分配,只增不減。為什麼這裡又不使用 realloc 而是使用 free + malloc?因為 realloc 除了重新分配記憶體之外,還會複製原有資料到新的記憶體塊中。這裡私有棧擴容之後,原有資料可以直接丟棄。

swapcontext 儲存當前 context 到 C->ctx,再啟用 S->main,也就是 main routine。

3.6 coroutine_status

該函數返回指定協程的狀態。正如前面所說,Dead 協程已經被銷燬,只是該函數返回 COROUTINE_DEAD。

3.7 coroutine_running

該函數返回協程組 S 正在執行的協程 id。

4 對協程庫的擴充套件

4.1 實現巢狀 resume

如果只有一個協程組是無法實現巢狀 resume 的。但是可以建立多個協程組來達到這個目的。以下圖所示的 resume 關係為例

stateDiagram main --> co_a : resume co_a --> main : yield co_a --> co_b : resume co_b --> co_a : yield

設計思路是 main 作為 co_a 的 resumer,co_a 作為 co_b 的 resumer,需要建立兩個協程組。具體程式碼如下:

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

struct args
{
    struct schedule *next_S;
    int next_co;
};

void fa(struct schedule *S, void *ud)
{
    struct args *arg = (struct args *)ud;
    printf("fa1\n");
    coroutine_resume(arg->next_S, arg->next_co);
    printf("fa2\n");
    coroutine_resume(arg->next_S, arg->next_co);
    printf("fa3\n");
    coroutine_yield(S);
    printf("fa4\n");
}

void fb(struct schedule *S, void *ud)
{
    struct args *arg = (struct args *)ud;
    printf("fb1\n");
    coroutine_yield(S);
    printf("fb2\n");
}

int main()
{
    struct args arg1 = {0};
    struct args arg2 = {0};

    struct schedule *S_a = coroutine_open();
    struct schedule *S_b = coroutine_open();

    int co_a = coroutine_new(S_a, fa, &arg1);
    int co_b = coroutine_new(S_b, fb, &arg2);

    arg1.next_S = S_b;
    arg1.next_co = co_b;

    printf("main start\n");

    while (coroutine_status(S_a, co_a))
    {
        coroutine_resume(S_a, co_a);
        printf("main\n");
    }

    printf("main end\n");
    coroutine_close(S_a);
    coroutine_close(S_b);
}

這段程式碼輸出:

main start
fa1
fb1
fa2
fb2
fa3
main
fa4
main
main end

4.2 實現對稱協程

使用非對稱協程可以實現對稱協程,但是這個協程庫的 yield 操作無法傳遞引數,只能藉助全域性變數。以下是實現程式碼:

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

#if __APPLE__ && __MACH__
#include <sys/ucontext.h>
#else
#include <ucontext.h>
#endif

#define STACK_SIZE (1024 * 1024)

struct schedule
{
	char stack[STACK_SIZE];
	ucontext_t main;
	int nco;
	int cap;
	int running;
	struct coroutine **co;
};

struct schedule_extra
{
	int target_co;
};

struct schedule_extra S_extra = {-1};

void co_symmetric_transfer(struct schedule *S, int id)
{
	if (coroutine_running(S) == -1)
	{
		// resumer call this func
		coroutine_resume(S, id);
		if (S_extra.target_co != -1 && coroutine_status(S, id))
		{
			co_symmetric_transfer(S, S_extra.target_co);
		}
	}
	else
	{
		// coroutine call this func
		S_extra.target_co = id;
		coroutine_yield(S);
	}
}

struct args
{
	int n;
	int co_other;
};

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 %d\n", coroutine_running(S), start + i, arg->co_other);
		co_symmetric_transfer(S, arg->co_other);
	}
}

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);

	arg1.co_other = co2;
	arg2.co_other = co1;

	printf("main start\n");

	co_symmetric_transfer(S, co1);
	co_symmetric_transfer(S, co2);

	printf("main end\n");
}

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

	return 0;
}

這段程式碼輸出:

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