ucontext-人人都可以实现的简单协程库
1.干貨寫在前面
協(xié)程是一種用戶態(tài)的輕量級(jí)線程。本篇主要研究協(xié)程的C/C++的實(shí)現(xiàn)。
首先我們可以看看有哪些語(yǔ)言已經(jīng)具備協(xié)程語(yǔ)義:
- 比較重量級(jí)的有C#、erlang、golang*
- 輕量級(jí)有python、lua、javascript、ruby
- 還有函數(shù)式的scala、scheme等。
c/c++不直接支持協(xié)程語(yǔ)義,但有不少開源的協(xié)程庫(kù),如:
Protothreads:一個(gè)“蠅量級(jí)” C 語(yǔ)言協(xié)程庫(kù)
libco:來自騰訊的開源協(xié)程庫(kù)libco介紹,官網(wǎng)
coroutine:云風(fēng)的一個(gè)C語(yǔ)言同步協(xié)程庫(kù),詳細(xì)信息
目前看到大概有四種實(shí)現(xiàn)協(xié)程的方式:
- 第一種:利用glibc 的 ucontext組件(云風(fēng)的庫(kù))
- 第二種:使用匯編代碼來切換上下文(實(shí)現(xiàn)c協(xié)程)
- 第三種:利用C語(yǔ)言語(yǔ)法switch-case的奇淫技巧來實(shí)現(xiàn)(Protothreads)
- 第四種:利用了 C 語(yǔ)言的 setjmp 和 longjmp( 一種協(xié)程的 C/C++ 實(shí)現(xiàn),要求函數(shù)里面使用 static local 的變量來保存協(xié)程內(nèi)部的數(shù)據(jù))
本篇主要使用ucontext來實(shí)現(xiàn)簡(jiǎn)單的協(xié)程庫(kù)。
2.ucontext初接觸
利用ucontext提供的四個(gè)函數(shù)getcontext(),setcontext(),makecontext(),swapcontext()可以在一個(gè)進(jìn)程中實(shí)現(xiàn)用戶級(jí)的線程切換。
本節(jié)我們先來看ucontext實(shí)現(xiàn)的一個(gè)簡(jiǎn)單的例子:
[cpp] view plain copy注:示例代碼來自維基百科.
保存上述代碼到example.c,執(zhí)行編譯命令:
gcc example.c -o example想想程序運(yùn)行的結(jié)果會(huì)是什么樣?
[plain] view plain copy上面是程序執(zhí)行的部分輸出,不知道是否和你想得一樣呢?我們可以看到,程序在輸出第一個(gè)“Hello world"后并沒有退出程序,而是持續(xù)不斷的輸出”Hello world“。其實(shí)是程序通過getcontext先保存了一個(gè)上下文,然后輸出"Hello world",在通過setcontext恢復(fù)到getcontext的地方,重新執(zhí)行代碼,所以導(dǎo)致程序不斷的輸出”Hello world“,在我這個(gè)菜鳥的眼里,這簡(jiǎn)直就是一個(gè)神奇的跳轉(zhuǎn)。
那么問題來了,ucontext到底是什么?
3.ucontext組件到底是什么
在類System V環(huán)境中,在頭文件< ucontext.h > 中定義了兩個(gè)結(jié)構(gòu)類型,mcontext_t和ucontext_t和四個(gè)函數(shù)getcontext(),setcontext(),makecontext(),swapcontext().利用它們可以在一個(gè)進(jìn)程中實(shí)現(xiàn)用戶級(jí)的線程切換。
mcontext_t類型與機(jī)器相關(guān),并且不透明.ucontext_t結(jié)構(gòu)體則至少擁有以下幾個(gè)域:
[cpp] view plain copy當(dāng)當(dāng)前上下文(如使用makecontext創(chuàng)建的上下文)運(yùn)行終止時(shí)系統(tǒng)會(huì)恢復(fù)uc_link指向的上下文;uc_sigmask為該上下文中的阻塞信號(hào)集合;uc_stack為該上下文中使用的棧;uc_mcontext保存的上下文的特定機(jī)器表示,包括調(diào)用線程的特定寄存器等。
下面詳細(xì)介紹四個(gè)函數(shù):
int getcontext(ucontext_t *ucp);初始化ucp結(jié)構(gòu)體,將當(dāng)前的上下文保存到ucp中
int setcontext(const ucontext_t *ucp);設(shè)置當(dāng)前的上下文為ucp,setcontext的上下文ucp應(yīng)該通過getcontext或者makecontext取得,如果調(diào)用成功則不返回。如果上下文是通過調(diào)用getcontext()取得,程序會(huì)繼續(xù)執(zhí)行這個(gè)調(diào)用。如果上下文是通過調(diào)用makecontext取得,程序會(huì)調(diào)用makecontext函數(shù)的第二個(gè)參數(shù)指向的函數(shù),如果func函數(shù)返回,則恢復(fù)makecontext第一個(gè)參數(shù)指向的上下文第一個(gè)參數(shù)指向的上下文context_t中指向的uc_link.如果uc_link為NULL,則線程退出。
void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...);makecontext修改通過getcontext取得的上下文ucp(這意味著調(diào)用makecontext前必須先調(diào)用getcontext)。然后給該上下文指定一個(gè)棧空間ucp->stack,設(shè)置后繼的上下文ucp->uc_link.
當(dāng)上下文通過setcontext或者swapcontext激活后,執(zhí)行func函數(shù),argc為func的參數(shù)個(gè)數(shù),后面是func的參數(shù)序列。當(dāng)func執(zhí)行返回后,繼承的上下文被激活,如果繼承上下文為NULL時(shí),線程退出。
int swapcontext(ucontext_t *oucp, ucontext_t *ucp);保存當(dāng)前上下文到oucp結(jié)構(gòu)體中,然后激活upc上下文。
如果執(zhí)行成功,getcontext返回0,setcontext和swapcontext不返回;如果執(zhí)行失敗,getcontext,setcontext,swapcontext返回-1,并設(shè)置對(duì)于的errno.
簡(jiǎn)單說來, getcontext獲取當(dāng)前上下文,setcontext設(shè)置當(dāng)前上下文,swapcontext切換上下文,makecontext創(chuàng)建一個(gè)新的上下文。
4.小試牛刀-使用ucontext組件實(shí)現(xiàn)線程切換
雖然我們稱協(xié)程是一個(gè)用戶態(tài)的輕量級(jí)線程,但實(shí)際上多個(gè)協(xié)程同屬一個(gè)線程。任意一個(gè)時(shí)刻,同一個(gè)線程不可能同時(shí)運(yùn)行兩個(gè)協(xié)程。如果我們將協(xié)程的調(diào)度簡(jiǎn)化為:主函數(shù)調(diào)用協(xié)程1,運(yùn)行協(xié)程1直到協(xié)程1返回主函數(shù),主函數(shù)在調(diào)用協(xié)程2,運(yùn)行協(xié)程2直到協(xié)程2返回主函數(shù)。示意步驟如下:
[cpp] view plain copy實(shí)現(xiàn)用戶線程的過程是:
下面代碼context_test函數(shù)完成了上面的要求。
[cpp] view plain copy在context_test中,創(chuàng)建了一個(gè)用戶線程child,其運(yùn)行的函數(shù)為func1.指定后繼上下文為main
func1返回后激活后繼上下文,繼續(xù)執(zhí)行主函數(shù)。
保存上面代碼到example-switch.cpp.運(yùn)行編譯命令:
g++ example-switch.cpp -o example-switch執(zhí)行程序結(jié)果如下
[cpp] view plain copy你也可以通過修改后繼上下文的設(shè)置,來觀察程序的行為。如修改代碼 child.uc_link = &main;
為
child.uc_link = NULL;再重新編譯執(zhí)行,其執(zhí)行結(jié)果為:
[cpp] view plain copy5.使用ucontext實(shí)現(xiàn)自己的線程庫(kù)
掌握了上一節(jié)從主函數(shù)到協(xié)程的切換的關(guān)鍵,我們就可以開始考慮實(shí)現(xiàn)自己的協(xié)程了。
定義一個(gè)協(xié)程的結(jié)構(gòu)體如下:
在定義一個(gè)調(diào)度器的結(jié)構(gòu)體
[cpp] view plain copy接下來,在定義幾個(gè)使用函數(shù)uthread_create,uthread_yield,uthread_resume函數(shù)已經(jīng)輔助函數(shù)schedule_finished.就可以了。
int uthread_create(schedule_t &schedule,Fun func,void *arg);創(chuàng)建一個(gè)協(xié)程,該協(xié)程的會(huì)加入到schedule的協(xié)程序列中,func為其執(zhí)行的函數(shù),arg為func的執(zhí)行函數(shù)。返回創(chuàng)建的線程在schedule中的編號(hào)。
void uthread_yield(schedule_t &schedule);掛起調(diào)度器schedule中當(dāng)前正在執(zhí)行的協(xié)程,切換到主函數(shù)。
void uthread_resume(schedule_t &schedule,int id);恢復(fù)運(yùn)行調(diào)度器schedule中編號(hào)為id的協(xié)程
int schedule_finished(const schedule_t &schedule);判斷schedule中所有的協(xié)程是否都執(zhí)行完畢,是返回1,否則返回0.注意:如果有協(xié)程處于掛起狀態(tài)時(shí)算作未全部執(zhí)行完畢,返回0.
代碼就不全貼出來了,我們來看看兩個(gè)關(guān)鍵的函數(shù)的具體實(shí)現(xiàn)。首先是uthread_resume函數(shù):
[cpp] view plain copy[cpp] view plain copy
更具體的代碼我已經(jīng)放到github上,點(diǎn)擊這里。
6.最后一步-使用我們自己的協(xié)程庫(kù)
保存下面代碼到example-uthread.cpp.
[cpp] view plain copy執(zhí)行編譯命令并運(yùn)行:
g++ example-uthread.cpp -o example-uthread ./example-uthread運(yùn)行結(jié)果如下:
[cpp] view plain copy總結(jié)一下,我們利用getcontext和makecontext創(chuàng)建上下文,設(shè)置后繼的上下文到主函數(shù),設(shè)置每個(gè)協(xié)程的棧空間。在利用swapcontext在主函數(shù)和協(xié)程之間進(jìn)行切換。
到此,使用ucontext做一個(gè)自己的協(xié)程庫(kù)就到此結(jié)束了。相信你也可以自己完成自己的協(xié)程庫(kù)了。
最后,代碼我已經(jīng)放到github上,點(diǎn)擊這里。
總結(jié)
以上是生活随笔為你收集整理的ucontext-人人都可以实现的简单协程库的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C++协程库coroutine使用指南
- 下一篇: C++ 协程与网络编程