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