云風(fēng)-coroutine源碼解析

??????最近不知道咋回事突然對(duì)協(xié)程的原理挺感興趣,雖然之前也學(xué)習(xí)過(guò)Go語(yǔ)言翠语,接觸過(guò)協(xié)程钦奋,但是其實(shí)并不太了解具體的原理,尤其看到知乎上stackless與stackful之間作比較的文章发侵,個(gè)人表示真心看不懂,但是帶著越是看不懂越要裝逼的沖動(dòng)妆偏,上網(wǎng)查了查一些協(xié)程的實(shí)現(xiàn)刃鳄,目前感覺(jué)比較知名的有:微信libcolibgo钱骂、Go語(yǔ)言作者之一的Russ Cox個(gè)人寫(xiě)的libtask铲汪、Java協(xié)程庫(kù)以及本文要剖析的云風(fēng)寫(xiě)的coroutine。不廢話(huà)罐柳,先上圖:

coroutine.png

從上圖可以看出coroutine有以下兩個(gè)特點(diǎn):
第一:代碼少掌腰,僅僅用不到300行代碼就實(shí)現(xiàn)了協(xié)程的核心邏輯,對(duì)此只有一個(gè)大寫(xiě)的服张吉。
第二:用純C語(yǔ)言寫(xiě)的(沒(méi)有c++)齿梁;說(shuō)實(shí)話(huà),個(gè)人自認(rèn)為C語(yǔ)言學(xué)得還行肮蛹,但是C++老是學(xué)不會(huì)勺择。
看懂了這個(gè),我感覺(jué)就可以看看其他稍微復(fù)雜點(diǎn)的實(shí)現(xiàn)(加入了poll等 因?yàn)槟壳斑@個(gè)簡(jiǎn)單的還沒(méi)法做到IO阻塞時(shí)自動(dòng)yield)伦忠。

在看之前省核,先聊點(diǎn)必備知識(shí)點(diǎn),這是理解協(xié)程實(shí)現(xiàn)的核心昆码。
getcontext气忠、setcontext、makecontext赋咽、swapcontext
這四個(gè)函數(shù)是Linux提供的系統(tǒng)函數(shù)(忘了跟大家說(shuō)了 云風(fēng)這個(gè)版本只可以在linux下運(yùn)行 雖然windows也有fiber這種東西 但是我感覺(jué)應(yīng)該木人感興趣....)旧噪,理解了這四個(gè)函數(shù),其實(shí)這個(gè)代碼基本80%的難點(diǎn)就搞定了(剩下20%在于要理解函數(shù)調(diào)用的棧機(jī)制)脓匿。
其實(shí)context這個(gè)詞在編程中是個(gè)很重要的詞淘钟,Java Spring框架中有ApplicationContext、OSGI框架中有BundleContext陪毡、線程也有自己存活的一個(gè)Context等等米母,其實(shí)context就是代表所必需的一個(gè)環(huán)境信息勾扭,有了context就可以決定你的一切走向。簡(jiǎn)單說(shuō)铁瞒,如果你所需要的僅僅是一個(gè)變量a=1妙色,那么我們也可以說(shuō)a=1就是你這個(gè)場(chǎng)景下的context。再比如線程切換的時(shí)候精拟,總不能讓線程切換回來(lái)的時(shí)候重新開(kāi)始吧,他曾經(jīng)走到過(guò)的地方虱歪,修改過(guò)的變量蜂绎,總得給人家保存到一個(gè)地方吧,那么這些需要保存的信息其實(shí)也是這個(gè)線程能繼續(xù)運(yùn)行所依賴(lài)的context笋鄙。只不過(guò)線程切換的涉及到CPU ring的變化(ring3 用戶(hù)態(tài) ring0 內(nèi)核態(tài))师枣,此時(shí)比較難處理的是每個(gè)ring下有自己的特用棧,而協(xié)程-作為輕量級(jí)的線程萧落,不涉及到CPU ring的轉(zhuǎn)變践美,僅僅在用戶(hù)態(tài)模擬了這種切換(待會(huì)兒分析完coroutine就知道啥意思啦),這也是協(xié)程會(huì)比線程性能高的本質(zhì)原因所在(Do less)找岖。

getcontext陨倡,顧名思義,將當(dāng)前context保存起來(lái)许布;setcontext兴革,改變當(dāng)前的context;makecontext這個(gè)畢竟牛逼蜜唾,是制作一個(gè)你想要的context杂曲;最后swapcontext從一個(gè)context切換到另外一個(gè)context。先來(lái)個(gè)簡(jiǎn)單例子練練手:

#include <stdio.h>
#include <ucontext.h>

void main(){
        char isVisit = 0;
        ucontext_t context1, context2; //ucontext_t也是linux提供的類(lèi)型 用來(lái)存儲(chǔ)當(dāng)前context的
        getcontext(&context1);
        printf("I come here\n");

        if(!isVisit){
                isVisit = 1;
                swapcontext(&context2, &context1);
        }

        printf("end of main!");
}

猜猜這個(gè)執(zhí)行結(jié)果是啥袁余? 先上答案:

result.png

可以看出 I come here執(zhí)行了兩次擎勘, why?我們來(lái)一句一句的解釋?zhuān)菏紫嚷暶髁藘蓚€(gè)變量context1和context2颖榜,可以把這兩個(gè)變量理解為容器棚饵,專(zhuān)門(mén)用來(lái)放當(dāng)前環(huán)境的,第三行的時(shí)候執(zhí)行了getcontext(&context1)意思是把當(dāng)前上下文環(huán)境保存到context1這個(gè)變量中掩完,然后打印了第一次“I come here”蟹地,然后運(yùn)行到isVisit,因?yàn)橐婚_(kāi)始isVisit=0藤为,所以會(huì)進(jìn)入if語(yǔ)句之中怪与,運(yùn)行到swapcontext時(shí)候,這個(gè)函數(shù)的含義是把當(dāng)前上下文環(huán)境保存到context2之中缅疟,同時(shí)跳到context1對(duì)應(yīng)的上下文環(huán)境之中分别。由于context1當(dāng)時(shí)保存的是getcontext調(diào)用時(shí)的上下文環(huán)境中(運(yùn)行程序到第幾行也是上下文環(huán)境中一個(gè)元素)遍愿,所以swapcontext調(diào)用完畢后,又會(huì)切換到printf對(duì)應(yīng)的這一行耘斩,再次打印出第二次的“I come here”沼填,但是此時(shí)之前isVisit已經(jīng)設(shè)置為1了,所以if不會(huì)進(jìn)入了括授,直接到“end of main”了坞笙,程序結(jié)束!其實(shí)這里還是有個(gè)東西沒(méi)有說(shuō)清楚荚虚,上下文環(huán)境到底包含什么薛夜? 我們知道真正執(zhí)行指令歸根結(jié)底還是CPU,而CPU在運(yùn)行程序時(shí)會(huì)借助于很多寄存器版述,比如EIP(永遠(yuǎn)指向下一條要執(zhí)行程序的地址)梯澜、ESP(stack pointer棧指針)、EBP(base pointer基址寄存器)等等渴析,這些其實(shí)就是所謂的上下文環(huán)境晚伙,比如EIP,程序在執(zhí)行時(shí)要靠這個(gè)寄存器指示下一步去執(zhí)行哪條指令俭茧,如果你切換到其他協(xié)程(線程也如此)咆疗,回來(lái)的時(shí)候EIP已經(jīng)找不到了,那你不悲劇了母债?那協(xié)程(線程也如此)豈不是又要重頭來(lái)過(guò)民傻?那這個(gè)時(shí)候顯然需要一塊內(nèi)存去把這個(gè)上下文環(huán)境保存住,下次回來(lái)的時(shí)候從內(nèi)存拿出來(lái)接著執(zhí)行就行场斑,就跟沒(méi)有切換一個(gè)樣漓踢。針對(duì)協(xié)程來(lái)說(shuō),上文Linux提供的ucontext_t這個(gè)變量就起到這個(gè)存儲(chǔ)上下文的作用(線程切換的上下文存儲(chǔ)涉及到ring的改變 由操作系統(tǒng)完成的 而協(xié)程切換上下文保存需要咱們自己搞定 操作系統(tǒng)根本不參與 不過(guò)這樣也減少了用戶(hù)態(tài)與內(nèi)核態(tài)的切換)漏隐。結(jié)合這段話(huà)再回頭看看上面的程序喧半,應(yīng)該會(huì)有新的體會(huì)。
Let us move forward青责,繼續(xù)再看下面一段程序:

#include <stdio.h>
#include <ucontext.h>

#define STACK_SIZE (1024*1024)
char stack[STACK_SIZE];


void test(int a){
  char dummy = 12;
  int    hello     = 9;
  printf("test %d\n",a);
}

void main(){
    char isVisit = 0;
    ucontext_t context1, context2;
    getcontext(&context1);
    printf("I come here\n");
    context1.uc_stack.ss_sp = stack;
    context1.uc_stack.ss_size = STACK_SIZE;
    context1.uc_link = &context2;
    makecontext(&context1, (void (*)(void))test, 1, 10);

    if(!isVisit){
        isVisit = 1;
        swapcontext(&context2, &context1);
        printf("I come to if!\n");
    }

    printf("end of main!\n");
}

先上結(jié)果:

I come here
test 10
I come to if!
end of main!

這里又加了一個(gè)makecontext挺据,這個(gè)可是context函數(shù)界的老大哥,協(xié)程核心全靠他脖隶,getcontext只是獲取一個(gè)當(dāng)前上下文扁耐,而makecontext卻能在得到當(dāng)前上下文的基礎(chǔ)上對(duì)其進(jìn)行社會(huì)主義改造,比如我們上面這段代碼中修改了棧的信息产阱,讓test函數(shù)執(zhí)行的時(shí)候使用stack[STACK_SIZE]作為其椡癯疲空間(函數(shù)調(diào)用過(guò)程需要棧保存返回地址 函數(shù)參數(shù) 局部變量),makecontext第三個(gè)參數(shù)代表test函數(shù)需要幾個(gè)參數(shù),因?yàn)槲覀兊膖est函數(shù)只需要一個(gè)int a王暗,所以這里傳入1悔据,后面的10就是具體參數(shù)值,再比如coroutine源碼中有下面一個(gè)片段:

makecontext(&C->ctx, (void (*)(void)) mainfunc, 2, (uint32_t)ptr, (uint32_t)(ptr>>32));

想必大家也知道第三個(gè)參數(shù)2啥意思了俗壹。這里比較重要的是兩個(gè)點(diǎn):
1. makecontext中第二個(gè)參數(shù)是一個(gè)函數(shù)指針科汗,意思是當(dāng)context1生效時(shí)便執(zhí)行test函數(shù)(可以理解為test函數(shù)執(zhí)行的上下文環(huán)境就是context1)。
2. 當(dāng)test函數(shù)執(zhí)行完畢后绷雏,會(huì)跳到context1.uc_link對(duì)應(yīng)的環(huán)境上下文(上面代碼設(shè)置為context2對(duì)應(yīng)的上下文)头滔。
這兩點(diǎn)需要用心體會(huì),結(jié)合上述代碼便可以更好的理解涎显。當(dāng)然坤检,我們這里用gdb調(diào)試一下,幫忙大家理解這個(gè)過(guò)程(有意搞了一個(gè)dummy和hello變量在test函數(shù) 貌似沒(méi)有用 實(shí)則很有用)棺禾。

具體操作見(jiàn)下圖:

image.png

幾個(gè)注意點(diǎn)缀蹄,一是注意gcc 別忘了加上-g參數(shù)峭跳,這是為了生成可以調(diào)試的信息膘婶,否則gdb無(wú)法調(diào)試。gdb常用的調(diào)試命令可以參照 gdb tutorial
常用就幾個(gè):b 加斷點(diǎn)蛀醉,比如上圖中我在test函數(shù)加了一個(gè)斷點(diǎn)悬襟,r是讓程序跑起來(lái),由于test加了斷點(diǎn)拯刁,所以在test函數(shù)第一行代碼停了下來(lái)(顯示的這一行代碼是馬上要執(zhí)行但是還未執(zhí)行的)脊岳,n是單步運(yùn)行,但是碰到函數(shù)并不會(huì)進(jìn)入垛玻,s也是單步運(yùn)行割捅,但是碰到函數(shù)可以step into進(jìn)入到函數(shù)內(nèi)部調(diào)試。p經(jīng)常使用帚桩,打印變量的值亿驾,有時(shí)還使用p/x 將打印的值以16進(jìn)制顯示。知道這些就夠用账嚎,接著看重要的細(xì)節(jié):

  1. p stack+0 打印stack的地址為 0x601080, p stack+1024*1024打印的地址為0x701080,而變量dummy的地址為0x70105f莫瞬,顯然這個(gè)地址正合適在stack+0與stack+1024*1024之間,我們知道局部變量都是在棧上分配的郭蕉,顯然test函數(shù)已經(jīng)使用stack作為棧來(lái)使用了疼邀,這也驗(yàn)證了makecontext制作的上下文環(huán)境已經(jīng)在生效。

2.如果dummy是在棧上分配的召锈,那么下一個(gè)變量hello肯定也應(yīng)該是在棧上分配吧旁振。注意看hello的地址0x701058顯然也是在stack范圍內(nèi)的。注意為啥hello地址比dummy地址少8呢?畢竟先聲明的是dummy變量规求,然后才是hello呀筐付?
這里有兩個(gè)點(diǎn):一是少,少是因?yàn)楝F(xiàn)在操作系統(tǒng)的棧增長(zhǎng)方向都是像地址減少的方向增加的阻肿,這個(gè)是目前通用的實(shí)現(xiàn)瓦戚,至于為啥無(wú)從考究。也就是說(shuō)丛塌,當(dāng)我們調(diào)用push命令壓棧一個(gè)元素较解,地址是減少的;二是為啥少8赴邻,那是因?yàn)槟壳拔沂褂玫膌inux是64位的印衔,棧一個(gè)元素占用8個(gè)字節(jié),雖然char只有一個(gè)字節(jié)姥敛,但是為了內(nèi)存對(duì)齊(方便硬件讀取 對(duì)齊后讀取快)银择,還是會(huì)在棧上給char分配8個(gè)字節(jié)(如果之前學(xué)過(guò)數(shù)字電子線路的同學(xué)應(yīng)該知道內(nèi)存地址編碼的問(wèn)題,如果不對(duì)齊曙求,硬件會(huì)花費(fèi)多次才能讀出想要的內(nèi)容卖词,所以現(xiàn)在一般都會(huì)提高讀取速度進(jìn)行內(nèi)存對(duì)齊)。
既然說(shuō)到棧了墨榄,其實(shí)棧是一種很有趣的數(shù)據(jù)結(jié)構(gòu)玄糟,太多東西都用到它了,比如JVM對(duì)字節(jié)碼的執(zhí)行袄秩、Vue源碼中解析模板生成AST阵翎、spring解析@Configure、@Import注解之剧、Tomcat對(duì)xml的解析(Digester類(lèi))等等郭卫,太多棧的使用場(chǎng)景,這可不是一句簡(jiǎn)單的先進(jìn)后出可以概括的背稼,里面有很多深刻的內(nèi)容贰军。而在函數(shù)調(diào)用這個(gè)場(chǎng)景里,操作系統(tǒng)通常會(huì)利用棧存儲(chǔ)返回地址雇庙、函數(shù)參數(shù)谓形、局部變量等消息(詳細(xì)分析可以參考 函數(shù)如何使用棧),我們只看一個(gè)比較重要的圖:

我在原圖基礎(chǔ)上加上高低地址的說(shuō)明疆前,比如main函數(shù)里面調(diào)用func_A,func_A調(diào)用func_B,此時(shí)棧的結(jié)構(gòu)就如上圖所示寒跳,我們可以發(fā)現(xiàn)棧里面有很多重要的東西,返回地址(要不然執(zhí)行完函數(shù)你根本不知道返回到哪兒 有了這個(gè)pop一下就拿到了返回地址)竹椒、局部變量(Java里面非逃逸對(duì)象也是直接在棧上分配喲 減輕GC的壓力)等童太,圖上還有個(gè)棧幀的概念,這個(gè)其實(shí)是軟件層面的界定,比如main用到的棧的范圍就是main棧幀书释,其他函數(shù)類(lèi)似翘贮,有了這個(gè)東西可以對(duì)某個(gè)函數(shù)能夠使用的棧空間進(jìn)行一定的界定爆惧。這不是重點(diǎn)所在狸页,重點(diǎn)就是要知道
1.棧是往地址小的方向增長(zhǎng)的(但這不是說(shuō)棧只往地址小的方向走,而是說(shuō)用到的時(shí)候往小的地方走扯再,函數(shù)調(diào)用完了芍耘,這個(gè)函數(shù)對(duì)應(yīng)的棧幀就沒(méi)用了,此時(shí)把棧指針加上一定的size即可熄阻,這時(shí)棧指針就往大的方向走啦)
2.局部變量是在棧上分配的

ok 要看懂coroutine源碼所需要的知識(shí)都說(shuō)完了斋竞,上源碼吧,先看看調(diào)度器以及協(xié)程(調(diào)度器負(fù)責(zé)協(xié)程切換的上下文保護(hù)工作)的結(jié)構(gòu)定義:

coroutine.c:
struct schedule {
    char stack[STACK_SIZE]; //椡貉常空間
    ucontext_t main; //存儲(chǔ)主上下文
    int nco; //存儲(chǔ)目前開(kāi)了多少個(gè)協(xié)程
    int cap;//容量 如果不夠了 會(huì)動(dòng)態(tài)擴(kuò)容 下面有實(shí)現(xiàn)
    int running;//目前調(diào)度器正在運(yùn)行著的協(xié)程的id
    struct coroutine **co;//指向一個(gè)指針數(shù)組其中數(shù)組每個(gè)元素又指向一個(gè)協(xié)程的信息
    //(協(xié)程信息看下面這個(gè)結(jié)構(gòu)體)
};

struct coroutine {
    coroutine_func func; //協(xié)程要執(zhí)行的函數(shù)體 
    void *ud;//
    ucontext_t ctx;//存儲(chǔ)當(dāng)前協(xié)程所對(duì)應(yīng)的上下文信息
    struct schedule * sch;//指向調(diào)度器 見(jiàn)上面的結(jié)構(gòu)體 每個(gè)協(xié)程總得知道是誰(shuí)在調(diào)度自己吧
    ptrdiff_t cap;//自己的棧的容量
    ptrdiff_t size;//自己的棧的真實(shí)使用size
    int status;//目前協(xié)程的狀態(tài) 源碼中有下面四種狀態(tài)
    char *stack;//當(dāng)前協(xié)程所使用的棧的指針
};


coroutine.h:
//協(xié)程的所有可能狀態(tài)
#define COROUTINE_DEAD 0
#define COROUTINE_READY 1
#define COROUTINE_RUNNING 2
#define COROUTINE_SUSPEND 3

說(shuō)一些細(xì)節(jié):
1 schedule結(jié)構(gòu)體中的棧信息是實(shí)打?qū)嵉囊粋€(gè)數(shù)組坝初,但是coroutine里的棧卻是棧指針,為啥這樣呢钾军?這就是coroutine的奧妙之處鳄袍,每個(gè)協(xié)程都使用調(diào)度器里面的空間作為其棧信息 ,然后調(diào)度到其他協(xié)程時(shí)巧颈,再為此協(xié)程分配內(nèi)存畦木,將調(diào)度器的棧的信息拷貝自己的的stack指針下保存起來(lái)袖扛,下次再調(diào)度自己時(shí)砸泛,再把stack指針的內(nèi)容原樣拷貝到調(diào)度器的stack數(shù)組里面,達(dá)到圓潤(rùn)切換的目的蛆封。

  1. 可以看到struct coroutine里面有個(gè)ptrdiff_t的類(lèi)型唇礁,這是個(gè)Linux系統(tǒng)提供的類(lèi)型,意思是存儲(chǔ)指針做減法后的數(shù)據(jù)類(lèi)型惨篱,因?yàn)檫@兩個(gè)字段是通過(guò)兩個(gè)指針做減法算出來(lái)的盏筐,后面可以明白這里的精巧用心。

接著讓我們看看初始化調(diào)度器的代碼:

coroutine.c:
#define DEFAULT_COROUTINE 16
struct schedule * 
coroutine_open(void) {
    struct schedule *S = malloc(sizeof(*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;
}

這里代碼邏輯比較簡(jiǎn)單砸讳,利用malloc為調(diào)度器分配空間琢融,然后賦上初值,然后為調(diào)度器里面的協(xié)程數(shù)組先分配默認(rèn)16個(gè)空間簿寂,可以看到cap為16漾抬,nco為0。這就是一種預(yù)分配策略常遂,就像Java里面的ArrayList似的纳令,先初始化一定大小,然后等數(shù)據(jù)超過(guò)擦破大小,再動(dòng)態(tài)擴(kuò)容平绩。coroutine里面動(dòng)態(tài)擴(kuò)容的邏輯在:

coroutine.c:
int 
coroutine_new(struct schedule *S, coroutine_func func, void *ud) {
    struct coroutine *co = _co_new(S, func , ud);
    if (S->nco >= S->cap) {
        int id = S->cap;
        S->co = realloc(S->co, S->cap * 2 * sizeof(struct coroutine *));
        memset(S->co + S->cap , 0 , sizeof(struct coroutine *) * S->cap);
        S->co[S->cap] = co;
        S->cap *= 2;
        ++S->nco;
        return id;
    } else {
        int i;
        for (i=0;i<S->cap;i++) {
            int id = (i+S->nco) % S->cap;
            if (S->co[id] == NULL) {
                S->co[id] = co;
                ++S->nco;
                return id;
            }
        }
    }
    assert(0);
    return -1;
}

這個(gè)函數(shù)的作用是為調(diào)度器生成新的協(xié)程圈匆,可以看到if的判斷,當(dāng)協(xié)程的數(shù)量要超過(guò)容量cap時(shí)捏雌,用了一個(gè)系統(tǒng)函數(shù)realloc重新再申請(qǐng)一塊更大的內(nèi)存(申請(qǐng)的大小是之前cap的兩倍)跃赚。
可以順路看一下Java ArrayList的擴(kuò)容玩法:

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

新的容量=老的容量+老的容量>>1, 右移相當(dāng)于除以2,新容量大約是老容量的1.5倍性湿。
ok来累,到了coroutine最精彩的一部分啦:

coroutine.c:
//此函數(shù)的作用是 讓調(diào)度器運(yùn)行協(xié)程號(hào)為id的協(xié)程的運(yùn)行
void 
coroutine_resume(struct schedule * S, int id) {
    assert(S->running == -1);
    assert(id >=0 && id < S->cap);
    struct coroutine *C = S->co[id];//通過(guò)協(xié)程id拿到協(xié)程結(jié)構(gòu)體信息 這個(gè)id是建立協(xié)程時(shí)返回的
    if (C == NULL)
        return;
    int status = C->status;
    switch(status) {
       /**有兩種狀態(tài)時(shí)可以讓協(xié)程恢復(fù)運(yùn)行 一是剛建立的時(shí)候 這時(shí)線程狀態(tài)是READY
 但是由于剛建立 棧信息還沒(méi)有配置好  所以利用getcontext以及makecontext為其配置上下文**/
    case COROUTINE_READY:
        getcontext(&C->ctx);
        C->ctx.uc_stack.ss_sp = S->stack;
        C->ctx.uc_stack.ss_size = STACK_SIZE;
        C->ctx.uc_link = &S->main;
        S->running = 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));
        swapcontext(&S->main, &C->ctx);
        break;
 /**還有一種就是之前運(yùn)行過(guò)了,然后被調(diào)度器調(diào)走了窘奏,運(yùn)行別的協(xié)程了嘹锁,
再次回來(lái)運(yùn)行此協(xié)程時(shí) 狀態(tài)變?yōu)镾USPEND 這個(gè)時(shí)候因?yàn)閯偨r(shí)棧信息已經(jīng)配置好了 所以這里不需要makecontext配置棧信息了 
只需要swapcontext切換到此協(xié)程就ok**/
    case COROUTINE_SUSPEND:
        memcpy(S->stack + STACK_SIZE - C->size, C->stack, C->size);//將協(xié)程內(nèi)部的棧數(shù)據(jù)拷貝到調(diào)度器的棧空間 這是為恢復(fù)本協(xié)程的執(zhí)行做準(zhǔn)備
        S->running = id;
        C->status = COROUTINE_RUNNING;
        swapcontext(&S->main, &C->ctx);
        break;
    default:
        assert(0);
    }
}

關(guān)鍵就是swapcontext進(jìn)行上下文的切換着裹,當(dāng)切換到C->ctx時(shí)领猾,由于之前為C->ctx配置的函數(shù)名是mainfunc,所以一切換就相當(dāng)于下一步去執(zhí)行mainfunc啦骇扇,同時(shí)2代表有兩個(gè)參數(shù)摔竿,然后后面兩個(gè)是調(diào)度器結(jié)構(gòu)的地址,由于是64位的機(jī)器少孝,這里將高低32位分別放到兩個(gè)變量里傳給mainfunc(這里如果不太明白繼續(xù)看開(kāi)頭的例子)继低。
而mainfunc我們可以想見(jiàn),肯定要去調(diào)用協(xié)程關(guān)聯(lián)的那個(gè)函數(shù):

coroutine.c:
static void
mainfunc(uint32_t low32, uint32_t hi32) {
    uintptr_t ptr = (uintptr_t)low32 | ((uintptr_t)hi32 << 32);
    struct schedule *S = (struct schedule *)ptr;
    int id = S->running;
    struct coroutine *C = S->co[id];
    C->func(S,C->ud);//調(diào)用協(xié)程要處理的業(yè)務(wù)函數(shù) 你自己寫(xiě)的
    _co_delete(C);
    S->co[id] = NULL;
    --S->nco;
    S->running = -1;
}

關(guān)鍵一步我已經(jīng)加上注釋?zhuān)瑘?zhí)行完協(xié)程肯定要銷(xiāo)毀資源稍走,下面那些都是在將調(diào)度器此協(xié)程id對(duì)應(yīng)的協(xié)程結(jié)構(gòu)體銷(xiāo)毀掉袁翁。還有個(gè)小細(xì)節(jié),這個(gè)函數(shù)作者前面加了一個(gè)static婿脸,由于此函數(shù)只在本文件(coroutine.c)內(nèi)部使用粱胜,所以作者加了static(有點(diǎn)像private那種feel static修飾的函數(shù)只可以在聲明所在的文件內(nèi)部使用)。

最最精彩的來(lái)了狐树,協(xié)程既然要切換焙压,肯定要有個(gè)方法讓出CPU吧,here it comes:

coroutine.c:
void
coroutine_yield(struct schedule * S) {
    int id = S->running;
    assert(id >= 0);
    struct coroutine * C = S->co[id];
    assert((char *)&C > S->stack);
    _save_stack(C,S->stack + STACK_SIZE);//讓出CPU最牛逼的一句 將當(dāng)前棧信息保存到協(xié)程結(jié)構(gòu)體里面的char *stack那個(gè)指針里面
    C->status = COROUTINE_SUSPEND;//讓出CPU后 狀態(tài)自然要是SUSPEND
    S->running = -1;
    swapcontext(&C->ctx , &S->main);//正式讓出CPU
}

static void
_save_stack(struct coroutine *C, char *top) {
    char dummy = 0;//神來(lái)之筆
    assert(top - &dummy <= STACK_SIZE);
    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);
}

上面這個(gè)最精彩就是_save_stack里面那個(gè)char dummy抑钟,這個(gè)變量有毛線用呢涯曲?
如果之前記得我們的例子,你應(yīng)該知道這個(gè)dummy我們關(guān)心不是他的值(這里等于1 在塔、2幻件、3....100 whatever 不重要),重要的是這個(gè)dummy的地址心俗,這個(gè)地址在哪兒呀傲武? 在棧上蓉驹,而別忘了棧是朝地址小的方向增長(zhǎng)的!>纠态兴!那么top一定比dummy的地址大(因?yàn)闂J浅刂沸〉姆较蛟鲩L(zhǎng)的),那么代碼中top-&dummy對(duì)應(yīng)一定是啥呀?那一定是這個(gè)協(xié)程執(zhí)行過(guò)程中產(chǎn)生的棧信息(肯定不能丟呀 丟了這個(gè)協(xié)程再次回過(guò)頭來(lái)懵逼了)疟位,所以下面利用memcpy函數(shù)將&dummy地址拷貝數(shù)據(jù)到C->stack里面(拷貝大小自然是top- &dummy 看得出這里才對(duì)協(xié)程中的char *stack進(jìn)行內(nèi)存分配 用時(shí)拷貝 一開(kāi)始你也不知道要分配多少合適呀 同時(shí)這里也解釋了為啥struct coroutine里面的cap和size變量是ptrdiff_t的類(lèi)型 因?yàn)檫@兩個(gè)字段都是靠指針做減法得到的)瞻润。這里的dummy變量真是天馬行空,想象力奇特的一種寫(xiě)法甜刻,小弟敬佩绍撞!
既然調(diào)度器里面棧信息已經(jīng)拷貝到協(xié)程結(jié)構(gòu)里面啦,那其他協(xié)程執(zhí)行時(shí)是不是可以隨意搞了得院,反正影響不到此協(xié)程了呀傻铣,真心是棒棒噠!

最后祥绞,看看作者在main函數(shù)里面給的例子吧:

main.c:
#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;
}

上圖中的foo函數(shù)相當(dāng)于業(yè)務(wù)代碼非洲,需要我們自己寫(xiě),這里循環(huán)一次讓出一次CPU蜕径,所以代碼執(zhí)行結(jié)果如下:

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

有沒(méi)有一種多線程運(yùn)行結(jié)果的既視感两踏? 但是可以看出代碼中既沒(méi)有使用fork函數(shù)(多進(jìn)程)也沒(méi)有使用pthread函數(shù)(多線程),操作系統(tǒng)壓根沒(méi)有感覺(jué)到你在切換(我們根本沒(méi)有進(jìn)入內(nèi)核態(tài))兜喻,我們?cè)谟脩?hù)態(tài)就實(shí)現(xiàn)了這種切換的feel梦染。真心是棒棒噠!

還有幾個(gè)小函數(shù)沒(méi)有講到朴皆,因?yàn)槟菐讉€(gè)真心太簡(jiǎn)單了帕识,有的一兩行,都是為上面的核心流程服務(wù)的车荔,理解了核心流程渡冻,那幾個(gè)函數(shù)自然而然的搞懂戚扳。
當(dāng)然這個(gè)版本畢竟比較簡(jiǎn)單忧便,還只能做到協(xié)程自己主動(dòng)讓出CPU,但是這么雷鋒的協(xié)程畢竟很少帽借,其實(shí)更多時(shí)候是碰到阻塞操作了(比如IO操作)珠增,需要讓出CPU,那就是要涉及IO多路復(fù)用啦砍艾,云風(fēng)大神只是給我們打個(gè)樣蒂教,讓我們理解這個(gè)版本之后往更復(fù)雜的版本邁進(jìn)!

先寫(xiě)到此吧脆荷,繼續(xù)擼libtask和libgo的代碼啦凝垛!有感再撰文懊悯!
I love coroutine!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市梦皮,隨后出現(xiàn)的幾起案子炭分,更是在濱河造成了極大的恐慌,老刑警劉巖剑肯,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件捧毛,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡让网,警方通過(guò)查閱死者的電腦和手機(jī)呀忧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)溃睹,“玉大人而账,你說(shuō)我怎么就攤上這事∫蚱” “怎么了福扬?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)惜犀。 經(jīng)常有香客問(wèn)我铛碑,道長(zhǎng),這世上最難降的妖魔是什么虽界? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任汽烦,我火速辦了婚禮,結(jié)果婚禮上莉御,老公的妹妹穿的比我還像新娘撇吞。我一直安慰自己,他們只是感情好礁叔,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布牍颈。 她就那樣靜靜地躺著,像睡著了一般琅关。 火紅的嫁衣襯著肌膚如雪煮岁。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,007評(píng)論 1 284
  • 那天涣易,我揣著相機(jī)與錄音画机,去河邊找鬼。 笑死新症,一個(gè)胖子當(dāng)著我的面吹牛步氏,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播徒爹,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼荚醒,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼芋类!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起界阁,我...
    開(kāi)封第一講書(shū)人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤梗肝,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后铺董,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體巫击,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年精续,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了坝锰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡重付,死狀恐怖顷级,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情确垫,我是刑警寧澤弓颈,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站删掀,受9級(jí)特大地震影響翔冀,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜披泪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一纤子、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧款票,春花似錦控硼、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至缚够,卻和暖如春幔妨,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背潮瓶。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工陶冷, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人毯辅。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像煞额,于是被迫代替她去往敵國(guó)和親思恐。 傳聞我的和親對(duì)象是個(gè)殘疾皇子沾谜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • Lua 5.1 參考手冊(cè) by Roberto Ierusalimschy, Luiz Henrique de F...
    蘇黎九歌閱讀 13,740評(píng)論 0 38
  • 本文主要是對(duì)自己學(xué)習(xí)協(xié)程并實(shí)現(xiàn)輕量級(jí)協(xié)程過(guò)程的一個(gè)記錄, 語(yǔ)言略顯啰嗦, 各位見(jiàn)諒. 水平有限, 如有疏漏, 歡迎...
    neilzwshen閱讀 4,699評(píng)論 1 11
  • 一. 人生三大問(wèn):我是誰(shuí),我從哪來(lái)胀莹,我到哪去基跑? 1.1. 協(xié)程是什么 我們知道,在現(xiàn)代計(jì)算機(jī)的世界里描焰,有進(jìn)程媳否,有線...
    cunfate閱讀 4,116評(píng)論 0 5
  • 本系列博客是本人的開(kāi)發(fā)筆記。為了方便討論荆秦,本人新建了一個(gè)微信群(iOS技術(shù)討論群)篱竭,想要加入的,請(qǐng)?zhí)砑颖救宋⑿牛簔...
    kyson老師閱讀 6,681評(píng)論 4 51
  • 前兩天阿里巴巴開(kāi)源了coobjc步绸,沒(méi)幾天就已經(jīng)2千多star了掺逼,我也看了看源碼,主要關(guān)注的是協(xié)程的實(shí)現(xiàn)瓤介,周末折騰了...
    小涼介閱讀 20,251評(píng)論 3 25