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++
