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スレッドを使用した人がインタフェースの意味を容易に理解できるようにした.
次に、キー構造体を見てみましょう.注釈はすべてはっきりと表記されている.
共有スタック
共有スタックという概念はこの論文「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が出てきて、それからこの協程の自分の桟になっても.明らかに、この時点でこの空間は他のコヒーレンスと共有されているので、「共有スタック」と呼ばれています.
hook
この方法は各スレッドライブラリにとって極めて重要であり,スレッド上で実行されるだけであり,同時的な特性はないため,hookが関数を保持しなければ,そのシステム呼び出しには依然として多くの時間がかかり,このようにスレッドの効率性を体現していない.hookとは、これらのシステム呼び出しをリロードし、自分たちが作成したコラボレーションライブラリに適合させ、タイマ、epoll_を使用することを意味します.waitなどの方法は、システム呼び出しのコヒーレントyieldをブロックし、別のコヒーレントを実行し、イベントが発生した場合、後で実行を続行します.libcoではいくつかの重要なIO関数しかhookしていないが,コヒーレンスが性能を体現できるところはIOにあることが分かったので,コヒーレンスは主にネットワークプログラミング,高性能サーバに用いられ,IO密集型プログラムには大きな助けがあり,CPU密集型プログラムには明らかな助けがない.
义齿
もう1つのc++がコンテキスト切替を実現するのはucontext法である.
ucontext例では交互印刷を10回実現し,協程間の切替問題を考慮するとapiの使用は便利であるが効率は低いことが分かる.
参考ブログ:
https://github.com/Tencent/libco
https://www.jianshu.com/p/837bb161793a
https://www.zhihu.com/question/52193579
まず、ユーザ状態とカーネル状態はそれぞれプログラムの実行中の2つの状態であり、スレッドがユーザプロセスアドレス空間の状態でプログラムを実行すると、ユーザ状態と呼ばれ、システム呼び出し、割り込み、異常などのイベントが発生すると、ユーザ状態からカーネル状態に変換され、カーネルアドレス空間に入って関数(システム呼び出し関数、割り込み処理関数、異常処理関数など)を実行します.すなわち,コヒーレント間のスイッチングはユーザ状態で実現され,ユーザ状態では現在の実行停止が実現され,現在の状態が保存され,論理ストリームを別の関数に導くなど一連のスイッチングプロセスが実現される.
プロセスの概要
スレッドとスレッドの関係は、スレッドとプロセスの関係を類比できます.
スレッドとスレッドの最大の違いは、スレッドがオペレーティングシステムのスケジューリングの最小単位であること、すなわち、スレッドがOSのスケジューリングの下で同時性を有すること、コパスは「ユーザ状態のスレッド」にすぎません「OSが直接スケジューリングに関与するわけではありません.つまり、コヒーレンスはシリアルで実行され、論理ストリームは1つしかありません.このとき、シリアルである以上、コヒーレンスの効率性はどこにあるのでしょうか.実際、コヒーレンスの最大の用途は、CPU密集型タスクではなくIO密集型タスクを処理することです.システム呼び出しを大量に呼び出す必要がある場合、プライマリ・コヒーレンスでyieldを実行し、cpuロジックをフローさせて他のコヒーレンスを実行し、クロックトリガまたはリソースが到着したときにプライマリ・コヒーレンスに戻って実行を継続します.したがって、現在のコラボレーションは、主に高性能サーバ側で高同時性のシナリオを処理するために使用されています.
コヒーレンスは、スケジューリング方法に基づいて、主に2つに分けることができる:対称コヒーレンスと非対称コヒーレンス.対称コンシステントはその名の通り、各コンシステントが平等であり、プライマリコンシステントと被コンシステントの違いがないため、ほとんどの対称コンシステントにはスケジューラがあり、スケジューラは一定のスケジューリングアルゴリズムに従ってコンシステントを処理し、go言語のGoroutineが典型である.非対称コヒーレンスとは、libcoを例に挙げると、呼び出し者コヒーレンスと被呼び出し者コヒーレンスの違いであり、呼び出し者アクティブyieldのみが呼び出し者を返し、他のコヒーレンスを呼び出すことなく呼び出します.
C++では、現在実装されているコンテキスト切り替えには主に3つの方法があります.
コモンスタックの方式.スレッドスタックのサイズはプロファイルによって決まり、デフォルトでは8 MBですが、コモンもスレッドのような静的スタックであれば、1つのスレッドで千万レベルのコモンを申請すると爆スタックが発生し、非常に不便です.現在、協程スタックの実現には主に3つの方法がある.
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