協程介紹
總所周知,協程這個概念已經是服務端開發領域中耳熟能詳的名詞了。說協程是一組程式元件,以往的多執行緒程式設計有個特點是需要來回進行系統級別的來回上下文切換,造成很大的系統開銷,不僅如此,很多操作我們還需要保證原子性,加鎖,鎖這個東西嘛,本來就是個坑,能不能最好還是不要用了。協程就是這麼牛,能解決上述出現的所有問題,因為協程是使用者態輕量級的多執行緒,上下文切換的開銷是非常小的,而且更重要的是,是使用者主動去進行切換的,因此不存在這個操作執行到一半,就被另一個執行緒給打斷了。那麼協程常見的用例都有哪些呢?當然可以用來做狀態機,可讀性很高。也可以用來做角色模型和產生器。大牛們也在不斷在造輪子,比如有非常牛逼的雲風大叔也造了一個簡易的協程框架,程式碼非常精簡,非常適合學習,真心點個贊!後面我們會著重分析雲風大叔的程式碼。
ucontext 上下文
下面我們來介紹幾個相關的 C 庫函式:setcontext
、getcontext
、makecontext
和 swapcontext
是用來做 context
控制的。setcontext
可以被看做是一個 setjmp
/longjmp
的高階版本。
在 ucontext.h 這個系統的標頭檔案上定義了 ucontext 的結構體,我們可以看到結構體如下所示:
typedef struct ucontext {
struct ucontext *uc_link;
sigset_t uc_sigmask;
stack_t uc_stack;
mcontext_t uc_mcontext;
...
} ucontext_t;
這是最重要的結構體,讓我們來分析一下這個這個結構體。
如果上下文被用 makecontext
來建立時,uc_link
指向的是當前上下文退出時候將會被 resumed
的上下文。uc_sigmask
被用來儲存在上下文中一組被阻塞的訊號, uc_stack
是一個被上下文使用的 stack
,uc_mcontext
用來儲存執行狀態,包括所有的暫存器和 CPU flags
、指令指標和棧指標。
函式介紹
int setcontext(const ucontext_t *ucp)
這個函式會把當前上下文轉移到上下文 ucp
中。該函式不會返回,從 ucp 這個指標中執行。
int getcontext(ucontext_t *ucp)
該函式會儲存當前的上下文資訊到 ucp 中。
void makecontext(ucontext_t *ucp, void *func(), int argc, ...)
在被之前使用 getcontext
初始化後的 ucp
中設定一個替代的控制執行緒, ucp.uc_stack
成員應該被指向合適大小的棧,常量 SIGSTKSZ
通常會被使用。當使用 setcontext
或 swapcontext
跳轉的時候,執行將從 func
指向的函式的入口點開始,當然別忘了指定 argc
引數,表示引數個數。當 func
終止的時候,控制權被返回到 ucp.uc_link
。
int swapcontext(ucontext_t *oucp, ucontext_t *ucp)
轉到 ucp
上下文中執行並儲存當前上下文到 oucp
中
下面我們來看一個簡單的示例:
#include <stdio.h>
#include <ucontext.h>
#include <unistd.h>
int main(int argc, const char *argv[]){
ucontext_t context;
getcontext(&context);
puts("Hello world");
sleep(1);
setcontext(&context);
return 0;
}
結果的輸出是:
Hello world
Hello world
Hello world
Hello world
…
是不是感覺這個世界很奇妙!
cloudwu C 協程
現在請把目光轉移到 c 協程
主要定義了幾個資料結構和函式,現在來分析一下如何實現的。
先建立一個結構體
struct schedule {
char stack[STACK_SIZE]; // 執行的協程的棧
ucontext_t main; // 下個要切換的協程的上下文狀態
int nco; // 當前協程的數目
int cap; // 協程總容量
int running; // 當前執行的協程
struct coroutine **co; // 協程陣列,指向指標的指標 co
};
struct coroutine {
coroutine_func func; // 呼叫函式
void *ud; // 使用者資料
ucontext_t ctx; // 儲存的協程上下文狀態
struct schedule * sch; // 儲存struct schedule指標
ptrdiff_t cap; // 上下文切換時儲存的棧的容量
ptrdiff_t size; // 上下文切換時儲存的棧的大小 size <= cap
int status; // 協程狀態
char *stack; // 儲存的棧
};
先呼叫 coroutine_open
來建立一個 schedule
結構體
struct schedule *
coroutine_open(void) {
struct schedule *S = malloc(sizeof(*S)); // S 是指標,*S 就是指標指向的結構體。
S->nco = 0;
S->cap = DEFAULT_COROUTINE;
S->running = -1;
S->co = malloc(sizeof(struct coroutine *) * S->cap);
memset(S->co, 0, sizeof(struct coroutine *) * S->cap);
return S;
}
後面呼叫 coroutine_new
來建立協程,如果當前的協程數目小於容量,直接加進去,否則,擴容為當前的2倍,並返回 id
。
後面就可以開始 resume
了,內部的實現細節是,先看看要執行的協程的狀態是什麼,如果是 ready
的話,那就先獲取當前的上下文資訊到協程的ctc中,設定棧,設定改協程終止時下一個要執行的協程,此處為 &S->main
。設定狀態為正在執行。設定該上下文指向的函式,此處為 mainfunc
,利用 swapcontext
去執行上下文 &C->ctx
並儲存當前的上下文資訊到 &S->main
。
總的來說,雲風大叔寫的程式碼十分通俗易懂,如有不明白的地方請留言,我將會盡快幫助您解答。