1.干貨寫在前面
協程是一種用戶態的輕量級線程。本篇主要研究協程的C/C++的實現。
首先我們可以看看有哪些語言已經具備協程語義:
- 比較重量級的有C#、erlang、golang*
- 輕量級有python、lua、javascript、ruby
- 還有函數式的scala、scheme等。
c/c++不直接支持協程語義,但有不少開源的協程庫,如:
Protothreads:一個“蠅量級” C 語言協程庫
libco:來自騰訊的開源協程庫libco介紹,官網
coroutine:云風的一個C語言同步協程庫,詳細信息
目前看到大概有四種實現協程的方式:
- 第一種:利用glibc 的 ucontext組件(云風的庫)
- 第二種:使用匯編代碼來切換上下文(實現c協程)
- 第三種:利用C語言語法switch-case的奇淫技巧來實現(Protothreads)
- 第四種:利用了 C 語言的 setjmp 和 longjmp( 一種協程的 C/C++ 實現,要求函數里面使用 static local 的變量來保存協程內部的數據)
本篇主要使用ucontext來實現簡單的協程庫。
2.ucontext初接觸
利用ucontext提供的四個函數getcontext(),setcontext(),makecontext(),swapcontext()可以在一個進程中實現用戶級的線程切換。
本節我們先來看ucontext實現的一個簡單的例子:
[cpp] view plain
copy #include?<stdio.h>??#include?<ucontext.h>??#include?<unistd.h>????int?main(int?argc,?const?char?*argv[]){??????ucontext_t?context;????????getcontext(&context);??????puts("Hello?world");??????sleep(1);??????setcontext(&context);??????return?0;??}??
注:示例代碼來自維基百科.
保存上述代碼到example.c,執行編譯命令:
gcc example.c -o example
想想程序運行的結果會是什么樣?
[plain] view plain
copy cxy@ubuntu:~$?./example???Hello?world??Hello?world??Hello?world??Hello?world??Hello?world??Hello?world??Hello?world??^C??cxy@ubuntu:~$??
上面是程序執行的部分輸出,不知道是否和你想得一樣呢?我們可以看到,程序在輸出第一個“Hello world"后并沒有退出程序,而是持續不斷的輸出”Hello world“。其實是程序通過getcontext先保存了一個上下文,然后輸出"Hello world",在通過setcontext恢復到getcontext的地方,重新執行代碼,所以導致程序不斷的輸出”Hello world“,在我這個菜鳥的眼里,這簡直就是一個神奇的跳轉。
那么問題來了,ucontext到底是什么?
3.ucontext組件到底是什么
在類System V環境中,在頭文件< ucontext.h > 中定義了兩個結構類型,mcontext_t和ucontext_t和四個函數getcontext(),setcontext(),makecontext(),swapcontext().利用它們可以在一個進程中實現用戶級的線程切換。
mcontext_t類型與機器相關,并且不透明.ucontext_t結構體則至少擁有以下幾個域:
[cpp] view plain
copy typedef?struct?ucontext?{??????struct?ucontext?*uc_link;??????sigset_t?????????uc_sigmask;??????stack_t??????????uc_stack;??????mcontext_t???????uc_mcontext;??????...??}?ucontext_t;??
當當前上下文(如使用makecontext創建的上下文)運行終止時系統會恢復uc_link指向的上下文;uc_sigmask為該上下文中的阻塞信號集合;uc_stack為該上下文中使用的棧;uc_mcontext保存的上下文的特定機器表示,包括調用線程的特定寄存器等。
下面詳細介紹四個函數:
int getcontext(ucontext_t
*ucp);
初始化ucp結構體,將當前的上下文保存到ucp中
int setcontext(
const ucontext_t *ucp);
設置當前的上下文為ucp,setcontext的上下文ucp應該通過getcontext或者makecontext取得,如果調用成功則不返回。如果上下文是通過調用getcontext()取得,程序會繼續執行這個調用。如果上下文是通過調用makecontext取得,程序會調用makecontext函數的第二個參數指向的函數,如果func函數返回,則恢復makecontext第一個參數指向的上下文第一個參數指向的上下文context_t中指向的uc_link.如果uc_link為NULL,則線程退出。
void makecontext(ucontext_t *ucp,
void (*func)(),
int argc, ...);
makecontext修改通過getcontext取得的上下文ucp(這意味著調用makecontext前必須先調用getcontext)。然后給該上下文指定一個棧空間ucp->stack,設置后繼的上下文ucp->uc_link.
當上下文通過setcontext或者swapcontext激活后,執行func函數,argc為func的參數個數,后面是func的參數序列。當func執行返回后,繼承的上下文被激活,如果繼承上下文為NULL時,線程退出。
int swapcontext(ucontext_t
*oucp, ucontext_t
*ucp);
保存當前上下文到oucp結構體中,然后激活upc上下文。
如果執行成功,getcontext返回0,setcontext和swapcontext不返回;如果執行失敗,getcontext,setcontext,swapcontext返回-1,并設置對于的errno.
簡單說來, getcontext獲取當前上下文,setcontext設置當前上下文,swapcontext切換上下文,makecontext創建一個新的上下文。
4.小試牛刀-使用ucontext組件實現線程切換
雖然我們稱協程是一個用戶態的輕量級線程,但實際上多個協程同屬一個線程。任意一個時刻,同一個線程不可能同時運行兩個協程。如果我們將協程的調度簡化為:主函數調用協程1,運行協程1直到協程1返回主函數,主函數在調用協程2,運行協程2直到協程2返回主函數。示意步驟如下:
[cpp] view plain
copy 執行主函數??切換:主函數?-->?協程1??執行協程1??切換:協程1??-->?主函數??執行主函數??切換:主函數?-->?協程2??執行協程2??切換協程2??-->?主函數??執行主函數??...??
這種設計的關鍵在于實現主函數到一個協程的切換,然后從協程返回主函數。這樣無論是一個協程還是多個協程都能夠完成與主函數的切換,從而實現協程的調度。
實現用戶線程的過程是:
我們首先調用getcontext獲得當前上下文修改當前上下文ucontext_t來指定新的上下文,如指定棧空間極其大小,設置用戶線程執行完后返回的后繼上下文(即主函數的上下文)等調用makecontext創建上下文,并指定用戶線程中要執行的函數切換到用戶線程上下文去執行用戶線程(如果設置的后繼上下文為主函數,則用戶線程執行完后會自動返回主函數)。
下面代碼context_test函數完成了上面的要求。
[cpp] view plain
copy #include?<ucontext.h>??#include?<stdio.h>????void?func1(void?*?arg)??{??????puts("1");??????puts("11");??????puts("111");??????puts("1111");????}??void?context_test()??{??????char?stack[1024*128];??????ucontext_t?child,main;????????getcontext(&child);???????child.uc_stack.ss_sp?=?stack;??????child.uc_stack.ss_size?=?sizeof(stack);??????child.uc_stack.ss_flags?=?0;??????child.uc_link?=?&main;????????makecontext(&child,(void?(*)(void))func1,0);????????swapcontext(&main,&child);??????puts("main");??}????int?main()??{??????context_test();????????return?0;??}??
在context_test中,創建了一個用戶線程child,其運行的函數為func1.指定后繼上下文為main
func1返回后激活后繼上下文,繼續執行主函數。
保存上面代碼到example-switch.cpp.運行編譯命令:
g++ example-
switch.cpp -o example-
switch
執行程序結果如下
[cpp] view plain
copy cxy@ubuntu:~$?./example-switch??1??11??111??1111??main??cxy@ubuntu:~$??
你也可以通過修改后繼上下文的設置,來觀察程序的行為。如修改代碼
child.uc_link = &main;
為
child
.uc_link =
NULL;
再重新編譯執行,其執行結果為:
[cpp] view plain
copy cxy@ubuntu:~$?./example-switch??1??11??111??1111??cxy@ubuntu:~$??
可以發現程序沒有打印"main",執行為func1后直接退出,而沒有返回主函數??梢?#xff0c;如果要實現主函數到線程的切換并返回,指定后繼上下文是非常重要的。
5.使用ucontext實現自己的線程庫
掌握了上一節從主函數到協程的切換的關鍵,我們就可以開始考慮實現自己的協程了。
定義一個協程的結構體如下:
[cpp] view plain
copy typedef?void?(*Fun)(void?*arg);????typedef?struct?uthread_t??{??????ucontext_t?ctx;??????Fun?func;??????void?*arg;??????enum?ThreadState?state;??????char?stack[DEFAULT_STACK_SZIE];??}uthread_t;??
ctx保存協程的上下文,stack為協程的棧,棧大小默認為DEFAULT_STACK_SZIE=128Kb.你可以根據自己的需求更改棧的大小。func為協程執行的用戶函數,arg為func的參數,state表示協程的運行狀態,包括FREE,RUNNABLE,RUNING,SUSPEND,分別表示空閑,就緒,正在執行和掛起四種狀態。
在定義一個調度器的結構體
[cpp] view plain
copy typedef?std::vector<uthread_t>?Thread_vector;????typedef?struct?schedule_t??{??????ucontext_t?main;??????int?running_thread;??????Thread_vector?threads;????????schedule_t():running_thread(-1){}??}schedule_t;??
調度器包括主函數的上下文main,包含當前調度器擁有的所有協程的vector類型的threads,以及指向當前正在執行的協程的編號running_thread.如果當前沒有正在執行的協程時,running_thread=-1.
接下來,在定義幾個使用函數uthread_create,uthread_yield,uthread_resume函數已經輔助函數schedule_finished.就可以了。
int uthread_create(schedule_t &schedule,Fun func,
void *arg);
創建一個協程,該協程的會加入到schedule的協程序列中,func為其執行的函數,arg為func的執行函數。返回創建的線程在schedule中的編號。
void uthread_yield(schedule_t &schedule);
掛起調度器schedule中當前正在執行的協程,切換到主函數。
void uthread_resume(schedule_t &schedule,
int id);
恢復運行調度器schedule中編號為id的協程
int schedule_finished(
const schedule_t &schedule);
判斷schedule中所有的協程是否都執行完畢,是返回1,否則返回0.注意:如果有協程處于掛起狀態時算作未全部執行完畢,返回0.
代碼就不全貼出來了,我們來看看兩個關鍵的函數的具體實現。首先是uthread_resume函數:
[cpp] view plain
copy void?uthread_resume(schedule_t?&schedule?,?int?id)??{??????if(id?<?0?||?id?>=?schedule.threads.size()){??????????return;??????}????????uthread_t?*t?=?&(schedule.threads[id]);????????switch(t->state){??????????case?RUNNABLE:??????????????getcontext(&(t->ctx));????????????????t->ctx.uc_stack.ss_sp?=?t->stack;??????????????t->ctx.uc_stack.ss_size?=?DEFAULT_STACK_SZIE;??????????????t->ctx.uc_stack.ss_flags?=?0;??????????????t->ctx.uc_link?=?&(schedule.main);??????????????t->state?=?RUNNING;????????????????schedule.running_thread?=?id;????????????????makecontext(&(t->ctx),(void?(*)(void))(uthread_body),1,&schedule);????????????????????????????case?SUSPEND:????????????????swapcontext(&(schedule.main),&(t->ctx));????????????????break;??????????default:?;??????}??}??
如果指定的協程是首次運行,處于RUNNABLE狀態,則創建一個上下文,然后切換到該上下文。如果指定的協程已經運行過,處于SUSPEND狀態,則直接切換到該上下文即可。代碼中需要注意RUNNBALE狀態的地方不需要break.
[cpp] view plain
copy void?uthread_yield(schedule_t?&schedule)??{??????if(schedule.running_thread?!=?-1?){??????????uthread_t?*t?=?&(schedule.threads[schedule.running_thread]);??????????t->state?=?SUSPEND;??????????schedule.running_thread?=?-1;????????????swapcontext(&(t->ctx),&(schedule.main));??????}??}??
uthread_yield掛起當前正在運行的協程。首先是將running_thread置為-1,將正在運行的協程的狀態置為SUSPEND,最后切換到主函數上下文。
更具體的代碼我已經放到github上,點擊這里。
6.最后一步-使用我們自己的協程庫
保存下面代碼到example-uthread.cpp.
[cpp] view plain
copy #include?"uthread.h"??#include?<stdio.h>????void?func2(void?*?arg)??{??????puts("22");??????puts("22");??????uthread_yield(*(schedule_t?*)arg);??????puts("22");??????puts("22");??}????void?func3(void?*arg)??{??????puts("3333");??????puts("3333");??????uthread_yield(*(schedule_t?*)arg);??????puts("3333");??????puts("3333");????}????void?schedule_test()??{??????schedule_t?s;????????int?id1?=?uthread_create(s,func3,&s);??????int?id2?=?uthread_create(s,func2,&s);????????while(!schedule_finished(s)){??????????uthread_resume(s,id2);??????????uthread_resume(s,id1);??????}??????puts("main?over");????}??int?main()??{??????schedule_test();????????return?0;??}??
執行編譯命令并運行:
g++ example-uthread.cpp -o example-uthread
./example-uthread
運行結果如下:
[cpp] view plain
copy cxy@ubuntu:~/mythread$./example-uthread??22??22??3333??3333??22??22??3333??3333??main?over??cxy@ubuntu:~/mythread$??
可以看到,程序協程func2,然后切換到主函數,在執行協程func3,再切換到主函數,又切換到func2,在切換到主函數,再切換到func3,最后切換到主函數結束。
總結一下,我們利用getcontext和makecontext創建上下文,設置后繼的上下文到主函數,設置每個協程的棧空間。在利用swapcontext在主函數和協程之間進行切換。
到此,使用ucontext做一個自己的協程庫就到此結束了。相信你也可以自己完成自己的協程庫了。
最后,代碼我已經放到github上,點擊這里。
總結
以上是生活随笔為你收集整理的ucontext-人人都可以实现的简单协程库的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。