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