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實現的一個簡單的例子:

  1. #include <stdio.h>  
  2. #include <ucontext.h>  
  3. #include <unistd.h>  
  4.   
  5. int main(int argc, const char *argv[]){  
  6.     ucontext_t context;  
  7.   
  8.     getcontext(&context);  
  9.     puts("Hello world");  
  10.     sleep(1);  
  11.     setcontext(&context);  
  12.     return 0;  
  13. }  
#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


想想程序運行的結果會是什麼樣?

  1. cxy@ubuntu:~$ ./example   
  2. Hello world  
  3. Hello world  
  4. Hello world  
  5. Hello world  
  6. Hello world  
  7. Hello world  
  8. Hello world  
  9. ^C  
  10. cxy@ubuntu:~$  
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_tucontext_t和四個函數getcontext(),setcontext(),makecontext(),swapcontext().利用它們可以在一個進程中實現用戶級的線程切換。


mcontext_t類型與機器相關,並且不透明.ucontext_t結構體則至少擁有以下幾個域:

  1. typedef struct ucontext {  
  2.     struct ucontext *uc_link;  
  3.     sigset_t         uc_sigmask;  
  4.     stack_t          uc_stack;  
  5.     mcontext_t       uc_mcontext;  
  6.     ...  
  7. } ucontext_t;  
           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返回主函數。示意步驟如下:

  1. 執行主函數  
  2. 切換:主函數 --> 協程1  
  3. 執行協程1  
  4. 切換:協程1  --> 主函數  
  5. 執行主函數  
  6. 切換:主函數 --> 協程2  
  7. 執行協程2  
  8. 切換協程2  --> 主函數  
  9. 執行主函數  
  10. ...  
    執行主函數
    切換:主函數 --> 協程1
    執行協程1
    切換:協程1  --> 主函數
    執行主函數
    切換:主函數 --> 協程2
    執行協程2
    切換協程2  --> 主函數
    執行主函數
    ...
這種設計的關鍵在於實現主函數到一個協程的切換,然後從協程返回主函數。這樣無論是一個協程還是多個協程都能夠完成與主函數的切換,從而實現協程的調度。


實現用戶線程的過程是:

  1. 我們首先調用getcontext獲得當前上下文
  2. 修改當前上下文ucontext_t來指定新的上下文,如指定棧空間極其大小,設置用戶線程執行完後返回的後繼上下文(即主函數的上下文)等
  3. 調用makecontext創建上下文,並指定用戶線程中要執行的函數
  4. 切換到用戶線程上下文去執行用戶線程(如果設置的後繼上下文爲主函數,則用戶線程執行完後會自動返回主函數)。


下面代碼context_test函數完成了上面的要求。

  1. #include <ucontext.h>  
  2. #include <stdio.h>  
  3.   
  4. void func1(void * arg)  
  5. {  
  6.     puts("1");  
  7.     puts("11");  
  8.     puts("111");  
  9.     puts("1111");  
  10.   
  11. }  
  12. void context_test()  
  13. {  
  14.     char stack[1024*128];  
  15.     ucontext_t child,main;  
  16.   
  17.     getcontext(&child); //獲取當前上下文  
  18.     child.uc_stack.ss_sp = stack;//指定棧空間  
  19.     child.uc_stack.ss_size = sizeof(stack);//指定棧空間大小  
  20.     child.uc_stack.ss_flags = 0;  
  21.     child.uc_link = &main;//設置後繼上下文  
  22.   
  23.     makecontext(&child,(void (*)(void))func1,0);//修改上下文指向func1函數  
  24.   
  25.     swapcontext(&main,&child);//切換到child上下文,保存當前上下文到main  
  26.     puts("main");//如果設置了後繼上下文,func1函數指向完後會返回此處  
  27. }  
  28.   
  29. int main()  
  30. {  
  31.     context_test();  
  32.   
  33.     return 0;  
  34. }  
#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);//修改上下文指向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


執行程序結果如下

  1. cxy@ubuntu:~$ ./example-switch  
  2. 1  
  3. 11  
  4. 111  
  5. 1111  
  6. main  
  7. cxy@ubuntu:~$  
cxy@ubuntu:~$ ./example-switch
1
11
111
1111
main
cxy@ubuntu:~$

你也可以通過修改後繼上下文的設置,來觀察程序的行爲。如修改代碼

child.uc_link = &main;


child.uc_link = NULL;


再重新編譯執行,其執行結果爲:

  1. cxy@ubuntu:~$ ./example-switch  
  2. 1  
  3. 11  
  4. 111  
  5. 1111  
  6. cxy@ubuntu:~$  
cxy@ubuntu:~$ ./example-switch
1
11
111
1111
cxy@ubuntu:~$
可以發現程序沒有打印"main",執行爲func1後直接退出,而沒有返回主函數。可見,如果要實現主函數到線程的切換並返回,指定後繼上下文是非常重要的。

5.使用ucontext實現自己的線程庫


掌握了上一節從主函數到協程的切換的關鍵,我們就可以開始考慮實現自己的協程了。

定義一個協程的結構體如下:

  1. typedef void (*Fun)(void *arg);  
  2.   
  3. typedef struct uthread_t  
  4. {  
  5.     ucontext_t ctx;  
  6.     Fun func;  
  7.     void *arg;  
  8.     enum ThreadState state;  
  9.     char stack[DEFAULT_STACK_SZIE];  
  10. }uthread_t;  
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,分別表示空閒,就緒,正在執行和掛起四種狀態。


在定義一個調度器的結構體

  1. typedef std::vector<uthread_t> Thread_vector;  
  2.   
  3. typedef struct schedule_t  
  4. {  
  5.     ucontext_t main;  
  6.     int running_thread;  
  7.     Thread_vector threads;  
  8.   
  9.     schedule_t():running_thread(-1){}  
  10. }schedule_t;  
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函數:

  1. void uthread_resume(schedule_t &schedule , int id)  
  2. {  
  3.     if(id < 0 || id >= schedule.threads.size()){  
  4.         return;  
  5.     }  
  6.   
  7.     uthread_t *t = &(schedule.threads[id]);  
  8.   
  9.     switch(t->state){  
  10.         case RUNNABLE:  
  11.             getcontext(&(t->ctx));  
  12.   
  13.             t->ctx.uc_stack.ss_sp = t->stack;  
  14.             t->ctx.uc_stack.ss_size = DEFAULT_STACK_SZIE;  
  15.             t->ctx.uc_stack.ss_flags = 0;  
  16.             t->ctx.uc_link = &(schedule.main);  
  17.             t->state = RUNNING;  
  18.   
  19.             schedule.running_thread = id;  
  20.   
  21.             makecontext(&(t->ctx),(void (*)(void))(uthread_body),1,&schedule);  
  22.   
  23.             /* !! note : Here does not need to break */  
  24.   
  25.         case SUSPEND:  
  26.   
  27.             swapcontext(&(schedule.main),&(t->ctx));  
  28.   
  29.             break;  
  30.         default: ;  
  31.     }  
  32. }  
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.


  1. void uthread_yield(schedule_t &schedule)  
  2. {  
  3.     if(schedule.running_thread != -1 ){  
  4.         uthread_t *t = &(schedule.threads[schedule.running_thread]);  
  5.         t->state = SUSPEND;  
  6.         schedule.running_thread = -1;  
  7.   
  8.         swapcontext(&(t->ctx),&(schedule.main));  
  9.     }  
  10. }  
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.

  1. #include "uthread.h"  
  2. #include <stdio.h>  
  3.   
  4. void func2(void * arg)  
  5. {  
  6.     puts("22");  
  7.     puts("22");  
  8.     uthread_yield(*(schedule_t *)arg);  
  9.     puts("22");  
  10.     puts("22");  
  11. }  
  12.   
  13. void func3(void *arg)  
  14. {  
  15.     puts("3333");  
  16.     puts("3333");  
  17.     uthread_yield(*(schedule_t *)arg);  
  18.     puts("3333");  
  19.     puts("3333");  
  20.   
  21. }  
  22.   
  23. void schedule_test()  
  24. {  
  25.     schedule_t s;  
  26.   
  27.     int id1 = uthread_create(s,func3,&s);  
  28.     int id2 = uthread_create(s,func2,&s);  
  29.   
  30.     while(!schedule_finished(s)){  
  31.         uthread_resume(s,id2);  
  32.         uthread_resume(s,id1);  
  33.     }  
  34.     puts("main over");  
  35.   
  36. }  
  37. int main()  
  38. {  
  39.     schedule_test();  
  40.   
  41.     return 0;  
  42. }  
#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


運行結果如下:

  1. cxy@ubuntu:~/mythread$./example-uthread  
  2. 22  
  3. 22  
  4. 3333  
  5. 3333  
  6. 22  
  7. 22  
  8. 3333  
  9. 3333  
  10. main over  
  11. cxy@ubuntu:~/mythread$  
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上,點擊這裏

轉自:http://blog.csdn.net/qq910894904/article/details/41911175

發佈了17 篇原創文章 · 獲贊 33 · 訪問量 20萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章