雲風coroutine源碼分析

前言

現在C++的開發開始流行使用coroutine,也就是協程。我看騰訊的幾個開源項目裏面都有協程的實現。使用協程可以用同步的寫法,達到異步的性能。它的基本原理其實就是在IO等待的時候切換出去,在適當的時刻再切換回來。雲風用200行代碼實現了一個最簡單的協程,我們先看這個代碼瞭解一下協程的原理,然後再看微信的libco實現。

協程簡單介紹

協程可以理解爲一個用戶級的線程,一個線程裏跑多個協程。協程分爲對稱協程非對稱協程,對稱協程就是當協程切換的時候他可以切換到任意其他的協程,比如goroutine,而非對稱協程只能切換到調用他的調度器。這裏實現的是一個非對稱協程。

coroutine源碼分析

詳細的註釋代碼在coroutine源碼註釋
我們先來看一下頭文件

#define COROUTINE_DEAD 0  //協程狀態
#define COROUTINE_READY 1
#define COROUTINE_RUNNING 2
#define COROUTINE_SUSPEND 3

struct schedule; //協程調度器

typedef void (*coroutine_func)(struct schedule *, void *ud); //協程執行函數

struct schedule * coroutine_open(void); //創建協程調度器
void coroutine_close(struct schedule *); //關閉協程調度器

int coroutine_new(struct schedule *, coroutine_func, void *ud); //用協程調度器創建一個協程
void coroutine_resume(struct schedule *, int id); //恢復id號協程
int coroutine_status(struct schedule *, int id); //返回id號協程的狀態
int coroutine_running(struct schedule *); //返回正在執行的協程id
void coroutine_yield(struct schedule *); //保存上下文後中斷當前協程執行

整個實現就這麼幾個函數。用法就是先使用coroutine_open創建協程調度器,然後coroutine_new協程,在協程中使用coroutine_yield中斷執行,同一個線程中使用coroutine_resume恢復協程執行。

接下來我們看具體的實現。

由於協程切換時需要保存當前上下文環境,這裏用到了ucontext這個庫,它有四個函數。

typedef struct ucontext {   //上下文結構體
    struct ucontext *uc_link;  // 該上下文執行完時要恢復的上下文
    sigset_t         uc_sigmask;  
    stack_t          uc_stack;  //使用的棧
    mcontext_t       uc_mcontext;  
    ...  
} ucontext_t;  
int getcontext(ucontext_t *ucp); //將當前上下文保存到ucp
int setcontext(const ucontext_t *ucp); //切換到上下文ucp
void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...); //修改上下文入口函數
int swapcontext(ucontext_t *oucp, ucontext_t *ucp); //保存當前上下文到oucp,切換到上下文ucp

源碼中比較簡單的函數就不說了,重點說幾個難理解的地方。

void 
coroutine_resume(struct schedule * S, int id) {
    assert(S->running == -1);
    assert(id >=0 && id < S->cap);
    struct coroutine *C = S->co[id];
    if (C == NULL)
        return;
    int status = C->status;
    switch(status) {
    case COROUTINE_READY: //如果狀態是ready也就是第一次創建
        getcontext(&C->ctx); //獲取當前上下文
        C->ctx.uc_stack.ss_sp = S->stack; //將協程棧設置爲調度器的共享棧
        C->ctx.uc_stack.ss_size = STACK_SIZE; //設置棧容量  使用時棧頂棧底同時指向S->stack+STACK_SIZE,棧頂向下擴張
        C->ctx.uc_link = &S->main; //將返回上下文設置爲調度器的上下文,協程執行完後會返回到main上下文
        S->running = id; //設置調度器當前運行的協程id
        C->status = COROUTINE_RUNNING; //設置協程狀態
        uintptr_t ptr = (uintptr_t)S;
        makecontext(&C->ctx, (void (*)(void)) mainfunc, 2, (uint32_t)ptr, (uint32_t)(ptr>>32));//重置上下文執行mainfunc
        swapcontext(&S->main, &C->ctx); //保存當前上下文到main,跳轉到ctx的上下文
        break;
    case COROUTINE_SUSPEND: //如果狀態時暫停也就是之前yield過
        memcpy(S->stack + STACK_SIZE - C->size, C->stack, C->size); //將協程之前保存的棧拷貝到調度器的共享棧
        S->running = id;
        C->status = COROUTINE_RUNNING;
        swapcontext(&S->main, &C->ctx); //同上
        break;
    default:
        assert(0);
    }
}

resume函數回覆協程的運行,協程運行時使用共享棧,中斷時將棧保存到私有棧中。

所謂的共享棧,就是將協程所使用的棧設爲調度器的棧,由於S->stack爲低地址,S->stack+STACK_SIZE爲高地址,使用時,棧頂棧底同時指向S->stack+STACK_SIZE,棧頂向低地址擴張。當yield時將已使用的棧拷貝到協程的私有棧當中,resume時將私有棧拷貝到S->stack中。

這樣做的好處是一個協程可以使用的棧空間很大,而且不會有提前分配導致的空間過大和浪費,只是拷貝對性能略有些影響。

C->ctx.uc_stack.ss_sp = S->stack; //將協程棧設置爲調度器的共享棧
C->ctx.uc_stack.ss_size = STACK_SIZE; //設置棧容量,使用時棧頂棧底同時指向S->stack+STACK_SIZE,棧頂向下擴張

這兩行將協程棧設置爲共享棧。

static void
_save_stack(struct coroutine *C, char *top) { //top爲棧底
    char dummy = 0; //這裏定義一個char變量,dummy地址爲棧頂
    assert(top - &dummy <= STACK_SIZE); //dummy地址減棧底地址爲當前使用的棧大小
    if (C->cap < top - &dummy) { //如果當前協程棧大小小於已用大小,重新分配
        free(C->stack);
        C->cap = top-&dummy;
        C->stack = malloc(C->cap);
    }
    C->size = top - &dummy;
    memcpy(C->stack, &dummy, C->size); //將共享棧拷貝到協程棧
}

這是保存棧的函數,這裏用了一個很巧妙的方法,定義了一個局部變量dummy,此時dummy的地址應該是棧頂,而top是棧底,這樣我們就知道當前協程使用的棧的大小。注意memcpy是從低地址開始拷貝的。

剩下的代碼就很簡單了,我在github上對源碼進行了詳細的註釋,只要理解了保存棧的過程應該就沒什麼難度了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章