[セットトップ]ucontext-誰もが実現できる簡単なコラボレーションライブラリ


1.干物は前に書く
コラボレーションは、ユーザ状態の軽量レベルスレッドです.本編では,コパスのC/C++の実現を主に検討する.まず、どの言語がすでに協力的な意味を持っているかを見ることができます.
重量級はC#、erlang、golang*軽量級はpython、lua、javascript、ruby 関数式のscala、schemeなどもあります.
c/c++は直接協程の意味をサポートしないが、Protothreads:ハエ級C言語協程庫libco:テンセントからのオープンソース協程庫libco紹介、公式サイトcoroutine:雲風のC言語同期協程庫、詳細
現在、約4つの協力を実現する方法が見られます.
1つ目:glibcを利用したucontextコンポーネント(雲風のライブラリ)2つ目は、アセンブリコードを使用してコンテキストを切り替える(cコヒーレンスを実現する)3つ目は、C言語文法switch-caseの奇淫テクニックを利用して実現(Protothreads)第四種類:C言語のsetjmpとlongjmp(1種の協程のC/C++実現を利用して、関数の中でstatic localの変数を使って協程内部のデータを保存することを要求する)本編では主にucontextを用いて簡単なコラボレーションライブラリを実現する.
2.ucontext初接触
ucontextによって提供される4つの関数getcontext(),setcontext(),makecontext(),swapcontext()は、1つのプロセスにおいてユーザレベルのスレッド切替を実現することができる.
このセクションでは、ucontext実装の簡単な例を見てみましょう.
#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

プログラムの実行結果を考えてみましょう.
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に復元したところでコードを再実行するので、プログラムの出力が絶えない「ああ、この菜鳥の目には、まるで不思議なジャンプだった.
では、問題が来ました.ucontextはいったい何ですか.
3.ucontextコンポーネントとは何か
クラスSystem V環境では、ヘッダファイルには、2つの構造タイプ、mcontext_tおよびucontext_tおよび4つの関数getcontext(),setcontext(),makecontext(),swapcontext()が定義.これらを使用すると、1つのプロセスでユーザーレベルのスレッド切り替えを実現できます.mcontext_tのタイプは機械に関連する、不透明である.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に格納されたコンテキストの特定のマシン表現は、スレッドを呼び出す特定のレジスタなどを含む.
次の4つの関数について詳しく説明します.
int getcontext(ucontext_t *ucp);

ucp構造体を初期化し、現在のコンテキストをucpに保存します.
int setcontext(const ucontext_t *ucp);

現在のコンテキストをucpに設定し、setcontextのコンテキストucpはgetcontextまたはmakecontextで取得し、呼び出しが成功した場合は返さない.コンテキストがgetcontext()を呼び出して取得された場合、プログラムはこの呼び出しを実行し続けます.コンテキストがmakecontextを呼び出して取得されると、プログラムはmakecontext関数の2番目のパラメータが指す関数を呼び出し、func関数が戻るとmakecontextの1番目のパラメータが指すコンテキストの1番目のパラメータが指すコンテキスト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コンポーネントを用いてスレッド切替を実現
コヒーレントはユーザ状態の軽量レベルスレッドと呼ぶが,実際には複数のコヒーレントは同じスレッドに属する.いずれの時点でも、同じスレッドで2つのコヒーレンスを同時に実行することはできません.コンシステントのスケジューリングを簡略化すると、メイン関数はコンシステント1を呼び出し、コンシステント1がメイン関数を返すまでコンシステント1を実行し、メイン関数はコンシステント2を呼び出し、コンシステント2がメイン関数を返すまでコンシステント2を実行します.手順は次のとおりです.
         
      :    -->   1
        1
      :  1  -->    
         
      :    -->   2
        2
        2  -->    
         
    ...

この設計の鍵は,主関数から1つのコヒーレントへの切り替えを実現し,次いでコヒーレントから主関数に戻ることにある.これにより、1つのコヒーレンスでも複数のコヒーレンスでも主関数との切り替えが完了し、コヒーレンスのスケジューリングが実現される.
ユーザー・スレッドを実装する手順は、次のとおりです.
まずgetcontextを呼び出して、現在のコンテキストを取得します.
現在のコンテキストucontext_の変更tは、スタック空間の極大サイズを指定する、ユーザスレッドの実行後に戻る後続コンテキスト(すなわち、主関数のコンテキスト)を設定するなどの新しいコンテキストを指定する.
makecontextを呼び出してコンテキストを作成し、ユーザースレッドで実行する関数を指定します.
ユーザスレッドコンテキストに切り替えてユーザスレッドを実行します(設定した後続コンテキストがメイン関数である場合、ユーザスレッドが実行されると自動的にメイン関数に戻ります).
次のコードcontext_test関数は、上記の要件を完了します.
#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では、func 1を実行するユーザスレッドchildが作成されます.後続コンテキストをmain func 1が戻った後に後続コンテキストをアクティブにし、メイン関数の実行を続行するように指定します.
上のコードをexample-switchに保存します.cpp.コンパイルコマンドを実行します.
g++ example-switch.cpp -o example-switch

実行プログラムの結果は以下の通りです.
cxy@ubuntu:~$ ./example-switch
1
11
111
1111
main
cxy@ubuntu:~$

後続コンテキストの設定を変更することで、プログラムの動作を観察することもできます.コードの変更
child.uc_link = &main;

を選択します.
child.uc_link = NULL;

再コンパイルの実行結果:
cxy@ubuntu:~$ ./example-switch
1
11
111
1111
cxy@ubuntu:~$

プログラムはmainを印刷していないことがわかり、func 1として実行された後、メイン関数を返さずに直接終了します.メイン関数からスレッドへの切り替えを実現して返す場合は、後続コンテキストを指定することが重要であることがわかります.
5.ucontextを使用して独自のスレッドライブラリを実現
前節の主関数からコヒーレンスへの切り替えの鍵を把握すると,我々は自分のコヒーレンスを実現することを考えることができる.コンカレントを定義する構造体は次のとおりです.
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を含み、それぞれ空き、準備、実行中、保留中の4つの状態を表す.
スケジューラを定義する構造体
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を返します.
コードが完全に貼られていないので、2つの重要な関数の具体的な実装を見てみましょう.まずはuthread_resume関数:
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.
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.
#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

実行結果は次のとおりです.
cxy@ubuntu:~/mythread$./example-uthread
22
22
3333
3333
22
22
3333
3333
main over
cxy@ubuntu:~/mythread$

プログラムコヒーレンスfunc 2がメイン関数に切り替わり、コヒーレンスfunc 3が実行され、メイン関数に切り替わり、func 2に切り替わり、メイン関数に切り替わり、func 3に切り替わり、最後にメイン関数に切り替わります.
まとめて、getcontextとmakecontextを使用してコンテキストを作成し、後続のコンテキストをメイン関数に設定し、各スレッドのスタック空間を設定します.swapcontextを用いて主関数とコヒーレンス間を切り替える.
これでucontextを用いて独自のコラボレーションライブラリを作成することはこれで終わります.あなたも自分で自分の協程庫を完成できると信じています.
最後に、コードはgithubに置いてあります.ここをクリックしてください.