cloudwu/coroutine 源碼分析

来源:https://www.cnblogs.com/mkckr0/archive/2022/06/06/16345774.html
-Advertisement-
Play Games

1 與其它協程庫使用對比 這個 C 協程庫是雲風(cloudwu) 寫的,其介面風格與 Lua 協程類似,並且都是非對稱 stackful 協程。這個是源代碼中的示例: #include "coroutine.h" #include <stdio.h> struct args { int n; }; ...


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

本文來自博客園,作者:mkckr0,轉載請註明原文鏈接:https://www.cnblogs.com/mkckr0/p/16345774.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 標簽+元素 1.標題標簽 段落標簽<h1> 一級標題 <h1><h2> 二級標題 <h2><h3> 三級標題 <h3><h4> 四級標題 <h4><h5> 五級標題 <h5><h6> 六級標題 <h6> 2.段落標簽 <p> 我是一個段落標簽 </p> //不換行 3.容器標簽 <div> 這是頭部 ...
  • props中的children屬性 組件標簽只用有子節點的時候,props就會有該屬性; children的屬性跟props一樣的,值可以是任意值;(文本,React元素,組件,函數) 組件: <ClassCom> 傳遞的數據 </ClassCom> 這樣的組件標簽中就會有子節點 props中的ch ...
  • Srinath,科學家,軟體架構師。Apache Axis2項目的聯合創始人,Apache Software基金會的成員,WSO2流處理器(wso2.com/analytics)的聯席架構師。 Srinath通過不懈的努力最終總結出了30條架構原則,他主張架構師的角色應該由開發團隊本身去扮演,而不... ...
  • 在《clickhouse專欄》上一篇文章中《資料庫、數據倉庫之間的區別與聯繫》,我們介紹了什麼是資料庫,什麼是數據倉庫,二者的區別聯繫。clickhouse的定位是“數據倉庫”,所以理解了上一篇的內容,其實就能夠知道clickhouse適用於什麼樣的應用場景,不適合什麼樣的應用場景。 下麵本節我們就 ...
  • 庫 是一種代碼的二進位的封裝形式,將.o文件打包封裝就成了庫。庫可以在任何地方使用,但用戶卻不能看見他的具體實現。庫有利於代碼模塊化,只要介面設計得合理,改變庫的內部實現,不會影響到用戶級別的代碼使用。 動態庫 1.封裝動態庫 假設有源代碼sum.c, sub.c gcc sum.c -c -o s ...
  • 目錄 一.簡介 二.效果演示 三.源碼下載 四.猜你喜歡 零基礎 OpenGL (ES) 學習路線推薦 : OpenGL (ES) 學習目錄 >> OpenGL ES 基礎 零基礎 OpenGL (ES) 學習路線推薦 : OpenGL (ES) 學習目錄 >> OpenGL ES 轉場 零基礎 O ...
  • Spring Ioc源碼分析系列--實例化Bean的幾種方法 前言 前面的文章Spring Ioc源碼分析系列--Bean實例化過程(二)在講解到bean真正通過那些方式實例化出來的時候,並沒有繼續分析了,而是留到了這裡去分析,主要是因為獲取獲取構造函數,推斷構造函數也是一個比較複雜的操作,就想另起 ...
  • 主線程與守護線程 預設情況下,Java 進程需要等待所有線程都運行結束,才會結束。有一種特殊的線程叫做守護線程,只要其它非守護線程運行結束了,即使守護線程的代碼沒有執行完,也會強制結束。 package Daemon; import lombok.extern.slf4j.Slf4j; @Slf4j ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...