教你在 C 語言上編寫本身的協程

協程介紹

總所周知,協程這個概念已是服務端開發領域中耳熟能詳的名詞了。說協程是一組程序組件,以往的多線程編程有個特色是須要來回進行系統級別的來回上下文切換,形成很大的系統開銷,不只如此,不少操做咱們還須要保證原子性,加鎖,鎖這個東西嘛,原本就是個坑,能不能最好仍是不要用了。協程就是這麼牛,能解決上述出現的全部問題,由於協程是用戶態輕量級的多線程,上下文切換的開銷是很是小的,並且更重要的是,是用戶主動去進行切換的,所以不存在這個操做執行到一半,就被另外一個線程給打斷了。那麼協程常見的用例都有哪些呢?固然能夠用來作狀態機,可讀性很高。也能夠用來作角色模型和產生器。大牛們也在不斷在造輪子,好比有很是牛逼的雲風大叔也造了一個簡易的協程框架,代碼很是精簡,很是適合學習,真心點個贊!後面咱們會着重分析雲風大叔的代碼。git

ucontext 上下文

下面咱們來介紹幾個相關的 C 庫函數:
setcontextgetcontextmakecontextswapcontext 是用來作 context 控制的。setcontext 能夠被看作是一個 setjmp/longjmp 的高級版本。github

在 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 是一個被上下文使用的 stackuc_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 一般會被使用。當使用 setcontextswapcontext 跳轉的時候,執行將從 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

總的來講,雲風大叔寫的代碼十分通俗易懂,若有不明白的地方請留言,我將會盡快幫助您解答。

相關文章
相關標籤/搜索