前言
現在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上對源碼進行了詳細的註釋,只要理解了保存棧的過程應該就沒什麼難度了。