日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 >

ucontext-人人都可以实现的简单协程库

發布時間:2025/3/21 39 豆豆
生活随笔 收集整理的這篇文章主要介紹了 ucontext-人人都可以实现的简单协程库 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

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;//指定??臻g??
  • ????child.uc_stack.ss_size?=?sizeof(stack);//指定棧空間大小??
  • ????child.uc_stack.ss_flags?=?0;??
  • ????child.uc_link?=?&main;//設置后繼上下文??
  • ??
  • ????makecontext(&child,(void?(*)(void))func1,0);//修改上下文指向func1函數??
  • ??
  • ????swapcontext(&main,&child);//切換到child上下文,保存當前上下文到main??
  • ????puts("main");//如果設置了后繼上下文,func1函數指向完后會返回此處??
  • }??
  • ??
  • 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);??
  • ??
  • ????????????/*?!!?note?:?Here?does?not?need?to?break?*/??
  • ??
  • ????????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-人人都可以实现的简单协程库的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。