Kilim是一個(gè)Java的actor框架,讓你可以在JVM里使用基于協(xié)程的actor模型,bluedavy曾經(jīng)介紹過(guò),這里不再贅言。這篇blog的目的在于分析下kilim實(shí)現(xiàn)的基本原理,看看怎么在JVM上實(shí)現(xiàn)協(xié)程。
在一些語(yǔ)言層面上支持協(xié)程的語(yǔ)言,如lua、ruby,都是直接在VM級(jí)別支持協(xié)程,VM幫你做context的保存和恢復(fù)。JVM沒(méi)有提供這樣的指令來(lái)保存和恢復(fù)方法棧的狀態(tài),因此kilim的實(shí)現(xiàn)還是需要在bytecode級(jí)別做文章。首先,試想下,如果是你來(lái)實(shí)現(xiàn)協(xié)程,你會(huì)怎么做?協(xié)程的兩個(gè)基本原語(yǔ)resume和yield,resume運(yùn)行協(xié)程,yield讓出執(zhí)行權(quán),下次resume的時(shí)候會(huì)從yield的地方重新執(zhí)行,并且context保持不變??梢?jiàn),你需要做這么幾個(gè)事情:
1、在yield的時(shí)候保存當(dāng)前context。
2、在resume的時(shí)候恢復(fù)context,并根據(jù)pc計(jì)數(shù)來(lái)決定從哪里恢復(fù)執(zhí)行。
3、半?yún)f(xié)程的實(shí)現(xiàn)來(lái)說(shuō),還需要一個(gè)調(diào)度器來(lái)調(diào)度所有協(xié)程。
4、為了做到用戶代碼透明,可能需要某種手段去修改用戶代碼,自動(dòng)幫你做上面三個(gè)事情。
kilim的實(shí)現(xiàn)就是干了這么幾個(gè)事情:
1、利用字節(jié)碼增強(qiáng),將普通的java代碼轉(zhuǎn)換為支持協(xié)程的代碼。
2、在調(diào)用pausable方法的時(shí)候,如果pause了就保存當(dāng)前方法棧的State,停止執(zhí)行當(dāng)前協(xié)程,將控制權(quán)交給調(diào)度器
3、調(diào)度器負(fù)責(zé)調(diào)度就緒的協(xié)程
4、協(xié)程resume的時(shí)候,自動(dòng)恢復(fù)State,根據(jù)協(xié)程的pc計(jì)數(shù)跳轉(zhuǎn)到上次執(zhí)行的位置,繼續(xù)執(zhí)行。
下面是來(lái)自kilim文檔的一個(gè)例子,同一段代碼,在字節(jié)碼增前前后的變化:
左邊是原始代碼,右邊是通過(guò)字節(jié)碼增強(qiáng)后的代碼。其中h方法是pausable的,也就是說(shuō)可能被暫停阻塞的,g方法因?yàn)檎{(diào)用了h這個(gè)方法也變成了pausable。
首先看,原始的h方法是沒(méi)有傳入任何參數(shù)的,增強(qiáng)后的代碼,多了個(gè)參數(shù)Fiber,指向當(dāng)前的協(xié)程,同樣,g方法本來(lái)只有一個(gè)參數(shù)n,現(xiàn)在在后面也多了個(gè)Fiber類型的參數(shù),同樣是指向當(dāng)前執(zhí)行的協(xié)程。
其次,在原始的g方法里,一旦調(diào)用,馬上進(jìn)入一個(gè)for循環(huán)。但是在增強(qiáng)后的代碼,多了個(gè)switch派發(fā)的過(guò)程,這就是前面提到的,根據(jù)當(dāng)前的Fiber的pc計(jì)數(shù),跳轉(zhuǎn)到上一次執(zhí)行的地方執(zhí)行。如果是第一次resume,也就是啟動(dòng)協(xié)程,那么就將初始循環(huán)的i設(shè)置為0,進(jìn)入原始代碼的循環(huán)部分。Fiber有一個(gè)pc計(jì)數(shù),稱為程序計(jì)數(shù)器,用于指向恢復(fù)context的時(shí)候需要跳轉(zhuǎn)到位置。
第三,在g方法里調(diào)用h這個(gè)可被暫停阻塞的方法的時(shí)候,在h方法前后多了一些調(diào)用:
f.down();
h(f);
f.up();
kilim的Fiber將每個(gè)pauseable方法的調(diào)用組織成一個(gè)棧,每個(gè)pauseable方法都有一個(gè)activation frame,翻譯過(guò)來(lái)可以稱為活動(dòng)棧幀,這個(gè)棧幀記錄了當(dāng)前的棧的State,注意這個(gè)棧跟java本身的方法調(diào)用棧區(qū)分開(kāi)來(lái),一個(gè)是VM層面的,一個(gè)是kilim框架層面的。這里的down方法就是將棧向下延伸,表示將調(diào)用一個(gè)pauseable方法,并且設(shè)置當(dāng)前State和pc計(jì)數(shù)。
調(diào)用了down之后,才是調(diào)用實(shí)際的h方法,最后還要調(diào)用一次up,顧名思義,就是說(shuō)一次pauseable方法調(diào)用完成,fiber的活動(dòng)棧要遞增一層,回到上一層。但是h方法調(diào)用可能出現(xiàn)四種情況:
1、正常的順利返回,沒(méi)有狀態(tài)需要恢復(fù),所謂NOT_PAUSING__NO_STATE
2、也是正常返回,有狀態(tài)需要恢復(fù),也就是NOT_PAUSING__HAS_STATE
3、h方法暫停阻塞,當(dāng)前沒(méi)有保存狀態(tài),需要保存狀態(tài),這是第一次暫停的時(shí)候,稱為PAUSING__NO_STATE
4、h方法暫停阻塞,當(dāng)前已經(jīng)有狀態(tài),不需要保存狀態(tài),這是第一次暫停之后的resume再次暫停,稱為PAUSING__HAS_STATE,通常不需要處理什么。
第四,可以看到,在up之后,就要根據(jù)up返回的上述4種狀態(tài)執(zhí)行不同的邏輯:
if (f.isPausing){
//第一次暫停,沒(méi)有狀態(tài)
if (!f.hasState){
//new一個(gè)State_I2,并保存i和n
f.state = new State_I2(i,n);
//記錄pc,還記的前面的switch嗎?
f.pc = H1;
}
return;
} else if (f.hasState)
//正常返回,有狀態(tài)需要恢復(fù),恢復(fù)i和n
State_I2 st = (State_I2) f.state;
i = st.i1; n = st.i2;
}
這里沒(méi)有處理NOT_PAUSING__NO_STATE和PAUSING__HAS_STATE,因?yàn)檫@兩種情況在這里不需要處理。
通過(guò)上面的分析,我想大家對(duì)kilim的實(shí)現(xiàn)應(yīng)該已經(jīng)有一個(gè)很基本的認(rèn)識(shí)。下一步,我們分析一個(gè)實(shí)際的代碼例子,查看整個(gè)運(yùn)作流程。