C++コパスの概要

7406 ワード

この記事では,C/C++のコパスパッケージングプロセスを実現するための新しい体験と,コパスの理解を学習したことを記録した.最初は「協程」という概念を知っていたgo言語では、多くの資料が「軽量級のユーザ状態スレッド」と記述していた.
まず、ユーザ状態とカーネル状態はそれぞれプログラムの実行中の2つの状態であり、スレッドがユーザプロセスアドレス空間の状態でプログラムを実行すると、ユーザ状態と呼ばれ、システム呼び出し、割り込み、異常などのイベントが発生すると、ユーザ状態からカーネル状態に変換され、カーネルアドレス空間に入って関数(システム呼び出し関数、割り込み処理関数、異常処理関数など)を実行します.すなわち,コヒーレント間のスイッチングはユーザ状態で実現され,ユーザ状態では現在の実行停止が実現され,現在の状態が保存され,論理ストリームを別の関数に導くなど一連のスイッチングプロセスが実現される.
プロセスの概要
スレッドとスレッドの関係は、スレッドとプロセスの関係を類比できます.
  • 1のプロセスには複数のスレッドがあり得る.同時に、1つのスレッドに複数のコモン
  • があるもよい.
  • 各スレッドには、関数パラメータ、戻りアドレス、レジスタ状態などを保存するための独自のプライベートスタックがあり、同時にスレッドは、スタック、データセグメント、コードセグメントなどの同じプロセス内のリソースを共有することができる.各スレッドには独自のスタックがあり、スレッドプロセスのリソースを共有することもできます.
  • 各スレッドには、その属性を格納し、スケジューリングする制御ブロックがある.協働にも
  • が必要です
    スレッドとスレッドの最大の違いは、スレッドがオペレーティングシステムのスケジューリングの最小単位であること、すなわち、スレッドがOSのスケジューリングの下で同時性を有すること、コパスは「ユーザ状態のスレッド」にすぎません「OSが直接スケジューリングに関与するわけではありません.つまり、コヒーレンスはシリアルで実行され、論理ストリームは1つしかありません.このとき、シリアルである以上、コヒーレンスの効率性はどこにあるのでしょうか.実際、コヒーレンスの最大の用途は、CPU密集型タスクではなくIO密集型タスクを処理することです.システム呼び出しを大量に呼び出す必要がある場合、プライマリ・コヒーレンスでyieldを実行し、cpuロジックをフローさせて他のコヒーレンスを実行し、クロックトリガまたはリソースが到着したときにプライマリ・コヒーレンスに戻って実行を継続します.したがって、現在のコラボレーションは、主に高性能サーバ側で高同時性のシナリオを処理するために使用されています.
    コヒーレンスは、スケジューリング方法に基づいて、主に2つに分けることができる:対称コヒーレンスと非対称コヒーレンス.対称コンシステントはその名の通り、各コンシステントが平等であり、プライマリコンシステントと被コンシステントの違いがないため、ほとんどの対称コンシステントにはスケジューラがあり、スケジューラは一定のスケジューリングアルゴリズムに従ってコンシステントを処理し、go言語のGoroutineが典型である.非対称コヒーレンスとは、libcoを例に挙げると、呼び出し者コヒーレンスと被呼び出し者コヒーレンスの違いであり、呼び出し者アクティブyieldのみが呼び出し者を返し、他のコヒーレンスを呼び出すことなく呼び出します.
    C++では、現在実装されているコンテキスト切り替えには主に3つの方法があります.
  • ucontext、fiberは、提供されたapiを採用し、この方式は比較的簡単で便利で、欠点はあまり効率的ではない
  • である.
  • はアセンブリ自身でコンテキスト切替を実現し、性能は効率的であるが、移植性が劣る
  • に至るまで下位層である.
  • setjump、longjump

  • コモンスタックの方式.スレッドスタックのサイズはプロファイルによって決まり、デフォルトでは8 MBですが、コモンもスレッドのような静的スタックであれば、1つのスレッドで千万レベルのコモンを申請すると爆スタックが発生し、非常に不便です.現在、協程スタックの実現には主に3つの方法がある.
  • 静的スタック、固定コヒーレンススタックのサイズ
  • コピースタックは、まずコヒーレントスタックのサイズを固定し、スタックのスペースが足りないことを発見したら、新しいメモリを拡張し、過去
  • までコピーします.
  • 共有スタック、複数のコヒーレンスは1つのメモリスタックを共有し、現在のコヒーレンスを実行する必要があるたびに、コヒーレンスのコンテキストを共有スタックにコピーし、コヒーレンスyieldの場合、コンテキストをコピーして保存します.欠点は、コヒーレント切り替えが遅く、複数のコヒーレントスタックコピー作業が必要であることです.

  • Libco解析
    libcoは微信で最も早くオープンソースのc++コヒーレンスライブラリであり、アセンブリを利用してコンテキストを実現する方式であり、同時にコヒーレンススタックは静的スタックと共有スタックが併存する方式を同時に採用している.
    ソース分析
    まずいくつかの重要な関数宣言と構造体を見てみましょう.次の図のいくつかの関数は、最も一般的なコプロセッサ呼び出しインタフェースであり、その具体的な使用については、例example_*を参照してください.まずlibcoはクラスpthreadのインタフェース設計を採用し,poxisスレッドを使用した人がインタフェースの意味を容易に理解できるようにした.
    int 	co_create( stCoRoutine_t **co,const stCoRoutineAttr_t *attr,void *(*routine)(void*),void *arg );	//    ,       stCoRoutine_t   、        、    “ ”  ,        coctx_t
    void    co_resume( stCoRoutine_t *co );																		//       ,       ,      ,        ,     
    void    co_yield( stCoRoutine_t *co );																		// cpu            ,        
    void    co_yield_ct(); 																						//ct = current thread,   co_yield  
    void    co_release( stCoRoutine_t *co );																	//    
    void    co_reset(stCoRoutine_t * co); 																		//    
    
    stCoRoutine_t *co_self();																					//      
    
    int		co_poll( stCoEpoll_t *ctx,struct pollfd fds[], nfds_t nfds, int timeout_ms );						//    ,    epoll_wait  
    void 	co_eventloop( stCoEpoll_t *ctx,pfn_co_eventloop_t pfn,void *arg );									//    ,  epoll_wait,      

    次に、キー構造体を見てみましょう.注釈はすべてはっきりと表記されている.
    struct stCoRoutineEnv_t							//      ,           
    {
    	stCoRoutine_t *pCallStack[ 128 ];			//    ,       
    	int iCallStackSize;
    	stCoEpoll_t *pEpoll;						//epoll  
    
    	//for copy stack log lastco and nextco
    	stCoRoutine_t* pending_co;                  //pending    
    	stCoRoutine_t* occupy_co;                   //    cpu   
    };
    
    struct stCoRoutine_t			//     
    {
    	stCoRoutineEnv_t *env;
    	pfn_co_routine_t pfn;       //    
    	void *arg;                  //    
    	coctx_t ctx;                //     
    
    	char cStart;
    	char cEnd;
    	char cIsMain;
    	char cEnableSysHook;
    	char cIsShareStack;
    
    	void *pvEnv;
    
    	//char sRunStack[ 1024 * 128 ];
    	stStackMem_t* stack_mem;
    
    
    	//save satck buffer while confilct on same stack_buffer;
    	char* stack_sp; 
    	unsigned int save_size;
    	char* save_buffer;
    
    	stCoSpec_t aSpec[1024];
    
    };
    
    

    共有スタック
    共有スタックという概念はこの論文「A Portable C++Library for Coroutine Sequencing」に最初に由来した.libcoでは、ユーザーは、新しいコヒーレンスを作成するときに、独自のコヒーレンススタックを持つか、他の任意の数のコヒーレンスと一緒に実行スタックを共有するかを選択できます.共有スタックの利点は、各コヒーレンスが独立した空間を占有する必要がなく、1つのスレッドでコヒーレンス数が急増すると、共有スタックがスタックを爆発させないという点である.これと同時に、共有スタックの欠点は、コヒーレンス間のコンテキスト切替に時間がかかることである.なぜなら、切替のたびにコヒーレンススタックの内容copy-in/copy-outを切り替えることでスタックの切替が実現されるからである.
    共有スタックの構造は、count要素があり、各要素はメモリを指すポインタstStackMem_である配列です.t .新規割当コンセンサス(co_create_env)では、割り当てたばかりのstShareStack_からtでは、RoundRobin方式(alloc_idx+)でstStackMem_を1つ取るtが出てきて、それからこの協程の自分の桟になっても.明らかに、この時点でこの空間は他のコヒーレンスと共有されているので、「共有スタック」と呼ばれています.
    struct stStackMem_t				//   
    {
    	stCoRoutine_t* occupy_co;	//            
    	int stack_size;
    	char* stack_bp; 			//stack_buffer + stack_size
    	char* stack_buffer;
    
    };
    
    struct stShareStack_t			//   
    {
    	unsigned int alloc_idx;		//index,          
    	int stack_size;
    	int count;
    	stStackMem_t** stack_array;	//    ,     count,            stStackMem_t   
    };

    hook
    この方法は各スレッドライブラリにとって極めて重要であり,スレッド上で実行されるだけであり,同時的な特性はないため,hookが関数を保持しなければ,そのシステム呼び出しには依然として多くの時間がかかり,このようにスレッドの効率性を体現していない.hookとは、これらのシステム呼び出しをリロードし、自分たちが作成したコラボレーションライブラリに適合させ、タイマ、epoll_を使用することを意味します.waitなどの方法は、システム呼び出しのコヒーレントyieldをブロックし、別のコヒーレントを実行し、イベントが発生した場合、後で実行を続行します.libcoではいくつかの重要なIO関数しかhookしていないが,コヒーレンスが性能を体現できるところはIOにあることが分かったので,コヒーレンスは主にネットワークプログラミング,高性能サーバに用いられ,IO密集型プログラムには大きな助けがあり,CPU密集型プログラムには明らかな助けがない.
    //5.hook syscall ( poll/read/write/recv/send/recvfrom/sendto )
    
    void 	co_enable_hook_sys();              //  hook
    void 	co_disable_hook_sys();             //  hook
    bool 	co_is_enable_sys_hook();           //      hook

    义齿
    もう1つのc++がコンテキスト切替を実現するのはucontext法である.
    ucontext      
    getcontext(ucontext_t *ucp)                ucp    
    setcontext(const ucontext_t *ucp)    ucp           
    makecontext(ucontext_t *ucp, void (*func)(), int argc, ...)          
    int swapcontext(ucontext_t *oucp, const ucontext_t *ucp)           oucp,     ucp     
    
    typedef struct ucontext{
        struct ucontext* uc_link;            ,           ,   NULL       
        sigset_t uc_sigmask;                    
        stack_t uc_stack;                   
        mcontext_t uc_mcontext;                
        ...
    }ucontext_t

    ucontext例では交互印刷を10回実現し,協程間の切替問題を考慮するとapiの使用は便利であるが効率は低いことが分かる.
    #include 
    #include 
    
    int stack[1024];
    static int num = 1;
    ucontext_t mn_cont,func_cont;
    void func(){
        while(1){
            swapcontext(&mn_cont,&func_cont);
            printf("func_context
    "); if(num++ >= 10){ break; } } printf("outof func
    "); } int main(){ getcontext(&mn_cont); mn_cont.uc_link=nullptr; mn_cont.uc_stack.ss_sp=stack; mn_cont.uc_stack.ss_size=sizeof(stack); makecontext(&mn_cont,func,0); while(1){ swapcontext(&func_cont,&mn_cont); printf("main_context
    "); } printf("out of main
    "); return 0; }

    参考ブログ:
        https://github.com/Tencent/libco
        https://www.jianshu.com/p/837bb161793a
        https://www.zhihu.com/question/52193579