主頁 > 後端開發 > cloudwu/coroutine 原始碼分析

cloudwu/coroutine 原始碼分析

2022-06-06 10:30:48 後端開發

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

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/486245.html

標籤:C++

上一篇:Apache無法在macOS上加載php7模塊-Monterey

下一篇:《Effective C++》閱讀總結(五): 繼承與面向物件設計&模板&記憶體&雜項討論

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more