Alibabaは2日前にcoobjcを開いて、数日で2,000以上の星を獲得しました。ソースコードも確認しました。主な懸念事項はコルーチンの実装です。週末に2日間を費やして、Goの前身であるlibtaskとAeolusに言及しました。コルーチンはその一部を理解し、いくつかの記事を読んで少し整理しました。
コルーチン
コルーチンは、特定の場所で実行を一時停止および再開するための複数のエントリポイントを許可することにより、非プリエンプティブマルチタスクのサブルーチンを一般化するコンピュータープログラムコンポーネントです。コルーチンは、協調的なタスク、例外、イベントループ、イテレータ、無限リスト、パイプなどの使い慣れたプログラムコンポーネントの実装に適しています。
プロセス->スレッド->コルーチン
コルーチン(コルーチン)コンパイラレベル、プロセス(プロセス)およびスレッド(スレッド)オペレーティングシステムレベル
プロセス(プロセス)とスレッド(スレッド)は、スケジューリングアルゴリズムによるosであり、現在のコンテキストを保存し、最後に中断された場所から計算を再開します。再開は予測できません。 CPU時間は関連しており、osに割り当てられたCPU時間になると、osによって強制的に中断され、開発者はそれらを正確に制御できなくなります。
コルーチンは、非プリエンプティブスケジューリングを実装する軽量のユーザーモードスレッドです。つまり、現在のコルーチンから他のコルーチンへの切り替えは、現在のコルーチンによって制御されます。現在のコルーチンフレームワークは、一般に1:Nモードとして設計されています。いわゆる1:Nは、複数のコルーチンを配置するコンテナーとしてのスレッドです。では、これらのコルーチンを時間内に切り替えるのは誰ですか?その答えは、一部のコルーチンはCPU自体をアクティブに放棄する、つまり各コルーチンプールにスケジューラがあり、このスケジューラは受動的にスケジュールされるということです。これは彼が派遣する率先しないことを意味します。そして、コルーチンがそれを実行できないことがわかった場合(ネットワークからのデータが返されるのを非同期的に待つが、まだデータがない場合など)、コルーチンはこのコルーチンによって通知されます。このとき、スケジューラのコードが実行され、スケジューリングコントローラーは、事前に設計されたスケジューリングアルゴリズムに従って、現在最も多くのCPUを必要とするコルーチンを見つけます。このコルーチンのCPUコンテキストを切り替えて、コルーチンが実行を待機する必要があるまで、または次のスケジューリングをトリガーするためにCPUをアクティブに委譲するAPIを呼び出すまで、CPUの実行能力をこのコルーチンに引き渡します。
長所と短所
利点:
- コルーチンはより軽量で、作成コストは小さく、メモリ消費量が削減されます。
コルーチン自体はユーザーモードで実行できます。各コルーチンのボリュームはスレッドよりもはるかに小さいため、プロセスはかなりの数のコルーチンに対応できます
- 協調型ユーザーモードスケジューラは、CPUコンテキストの切り替えオーバーヘッドを削減し、CPUキャッシュヒット率を向上させます
。プリエンプティブスケジューリングに対する協調型スケジューリングの利点は、コンテキスト切り替えのオーバーヘッドが少なく、キャッシュをホットで実行しやすくなることです。マルチスレッドと比較して、スレッドの数が多いほど、コルーチンのパフォーマンス上の利点が明白になります。プロセス/スレッドの切り替えはカーネルで行う必要がありますが、コルーチンは必要ありませんコルーチンは、より軽量で高速なユーザーモードスタックによって実現されます。重いI / Oプログラムには大きな利点があります。たとえば、クローラーで数百のスレッドを開くと速度が大幅に遅くなりますが、コルーチンを開くと遅くなりません。
ただし、コルーチンはネイティブスレッドの優先度の概念も放棄します。長時間のコンピューティングタスクがある場合、カーネルスケジューラは常にIOタスクに優先順位を付けるため、できるだけ早く応答するため、IOタスクの応答遅延に影響します。このスレッドにCPUを集中的に使用するコルーチンがあると仮定すると、彼にはIO操作がありません。つまり、スケジューラによってスケジュールされたプロセスをアクティブにトリガーせず、他のコルーチンを実行できない場合があるため、この場合はプログラマは自分でそれを避ける必要があります。
さらに、シングルスレッドコルーチンソリューションでは、レイテンシに影響を与えるすべての要因であるファイル操作やメモリページフォールトなどのブロックを根本的に回避することはできません。
- 同期ロックを削減し、全体としてパフォーマンスを向上させる
コルーチンソリューションは、イベントループスキームに基づいており、同期ロックの頻度を削減します。ただし、競争がある場合、クリティカルセクションは保証されないため、ロックされる場所にはコルーチンロックを追加する必要があります。
- 同期の考え方に従って非同期コードを記述できます。つまり、同期ロジックを使用し、コルーチンによってスケジュールされたコールバックを記述できます。
コルーチンは実際にコールバックの使用を減らすことができますが、完全に置き換えることはできないことに注意してください。イベント駆動型プログラミングでコルーチンを使用する代わりに、コールバックの方が適しています。
短所:
- コルーチンの実行にはブロッキング操作があってはなりません。そうしないと、スレッド全体がブロックされます(コルーチンは言語レベル、スレッド、プロセスはオペレーティングシステムレベルです)。
- グローバル変数とオブジェクト参照の使用に特別な注意を払う必要がある
- コルーチンはIO集約型プログラムの効率を処理できますが、CPU集約型を処理することはその長所ではありません。
このスレッドにCPUを集中的に使用するコルーチンがあると仮定すると、彼にはIO操作がありません。つまり、スケジューラによってスケジュールされたプロセスをアクティブにトリガーせず、他のコルーチンを実行できない場合があるため、この場合はプログラマは自分でそれを避ける必要があります。
該当するシーン
- 高性能コンピューティングは、スループットと引き換えに公平性を犠牲にします。高性能コンピューティングの分野からのコルーチンの初期の成功例であるコラボレーティブスケジューリングは、プリエンプティブスケジューリングと比較して、公平性を犠牲にしてスループットと交換できます。
- IOバインドのタスク
IO集中型プログラムでは、IO操作はCPUの操作よりもはるかに小さいため、多くの場合、CPUはIO操作を待機する必要があります。同期IOでは、オペレーティングシステムがIOプロセス中に他のことを実行できるように、システムがスレッドを切り替える必要があります。このように、コードは人間の思考習慣に沿ったものですが、大量のスレッド切り替えにより、パフォーマンスが大幅に低下します。
したがって、人々は非同期IOを発明しました。コールバックをトリガーするのはデータが到着したときです。スレッドの切り替えによるパフォーマンスの低下を減らすため。しかし、この種の欠点も大きなものです。最大の問題は、人間の線形思考モードを破壊することです。論理的な線形プロセスをいくつかのフラグメントに分割する必要があります。各フラグメントの開始と終了は非同期イベントです。終了して開始します。ある程度のトレーニングを経て、この考え方に適応することはできますが、それでも精神的な負担を増やす必要があります。人間の思考モードに対応して、最も一般的なプログラミング言語は必須であり、プログラム自体はほぼ線形の構造を示します。非同期コールバックは、思考の連続性を同時に破壊するだけでなく、プログラムの連続性も破壊するため、プログラムを読み取るときにより多くのエネルギーを費やすことができます。これらの要素はソフトウェアプロジェクトの追加のメンテナンスコストであるため、ほとんどの企業はnode.jsやRxJavaなどの非同期コールバックフレームワークを好みませんが、これらのフレームワークはプログラムの同時実行性を向上させることができます。
しかし、コルーチンはこの問題をうまく解決できます。たとえば、コルーチンとしてIO操作を記述します。IO操作がトリガーされると、CPUは自動的に他のコルーチンに割り当てられます。コルーチンの切り替えは非常に軽いことに注意してください。コルーチンは、非同期IOのこのカプセル化を通じてパフォーマンスを維持し、コードの簡単な書き込みと読みやすさも保証します。
- ジェネレータースタイルのストリーミングコンピューティングは
、コールバック地獄(コールバック地獄)を排除し、同期モデルを使用して開発コストを削減しながら、同時に3つの要求を送信するなど、より柔軟な制御フローの利点を維持します。現時点では、スタックを経済的に使用することで、「光」を十分に活用できますボリューム「利点。
ucontext
コルーチンには通常、2種類の実装があります。1つはスタックレスで、もう1つはスタックフルです。
構造
struct ucontext {
/*
* Keep the order of the first two fields. Also,
* keep them the first two fields in the structure.
* This way we can have a union with struct
* sigcontext and ucontext_t. This allows us to
* support them both at the same time.
* note: the union is not defined, though.
*/
sigset_t uc_sigmask; //这个上下文要阻塞的信号
mcontext_tt uc_mcontextt; //保存的上下文的特定机器表示,包括调用线程的特定寄存器等
struct __ucontext *uc_link; //指向当前的上下文结束时要恢复到的上下文
stack_t uc_stack; //该上下文中使用的栈
int __spare__[8];
};
getcontext
int getcontext(ucontext_t *ucp)
この関数ucp
は、ポイントされた構造ucontext_t
(以前の実行状態のコンテキストを保存するために使用される)を初期化し、現在有効なコンテキストに入力します
setcontext
int setcontext(const ucontext_t *ucp)
この関数は、ユーザーコンテキストucp
を指定されたコンテキストに復元します。成功した呼び出しは返されません。ucp
指し示されるコンテキストは、getcontext()またはmakecontext()によって生成される必要があります。
コンテキストがgetcontext()によって生成されている場合は、コンテキストに切り替えて、プログラムの実行をgetcontext()の後に続行します。
コンテキストがmakecontext()によって生成された場合は、コンテキストに切り替え、プログラムの実行は、makecontext()の呼び出しで指定された2番目のパラメーターを持つ関数に切り替えます。関数が戻ると、makecontext()の最初のパラメーターのuc_link
コンテキストでポイントされているコンテキストを渡します。の場合NULL
、手順は終了します。
成功した場合、getcontext()は0を返し、setcontext()は返しません。エラーの場合、どちらも-1を返し、適切なerrnoを割り当てます。
makecontext
void makecontext(ucontext_t *ucp, void (*func)(void), int argc, ...)
関数の変更ucp
によって示されるコンテキストは、getcontext()ucp
によって初期化されたコンテキストです。このコンテキストがswapcontext()またはsetcontext()を使用して復元されると、プログラムの実行は、makecontext()呼び出しによって渡されたパラメーターの呼び出しに切り替わります。makecontext()が呼び出しを行う前に、アプリケーションはコンテキストのスタック割り当てが変更されていることを確認する必要があります。アプリケーションは、値が渡された値と同じであることを確認する必要があります(パラメーターの値はすべて4バイトです)。そうでない場合、未定義の動作が発生します。makecontext()の変更されたコンテキストが戻ると、コンテキストを復元するかどうかを決定するために使用されます。makecontext()を呼び出す前に、アプリケーションを初期化する必要があります。func
argc
func
argc
func
int
uc_link
uc_link
スワップコンテキスト
int swapcontext(ucontext_t *restrict oucp, const ucontext_t *restrict ucp)
この関数は、現在のコンテキストを指定oucp
されたデータ構造に保存しucp
、指定されたコンテキストに設定します。
正常に完了すると、swapcontext()は0を返します。それ以外の場合は、-1を返し、適切なerrnoを割り当てます。
swapcontext()関数は、次の理由で失敗する可能性があります
。ENOMEMucpパラメータに、操作を完了するのに十分なスタックスペースがない
ucontextコルーチンの実際の使用
getcontext、makecontext、swapcontextをluaと同様の協調コルーチンにカプセル化します。コードでCPUを積極的に生成する必要があります。
コルーチンのスタックは、ヒープ割り当てにmallocを使用します。割り当てられたスペースは、64ビットシステムでのスタックの使用と一致します。アドレスは減分されます。uc_stack.uc_sizeによって設定されたサイズは、実際にはあまり効果がないようです。使用すると、割り当てられたヒープを超えます。このサイズは、小さいアドレスのヒープに対して引き続き使用されます。現時点では、ヒープメモリの範囲外の使用が発生します。以前にヒープに割り当てられたデータを変更すると、さまざまな予測できない動作が発生します。コアダンプの後は、実際の理由はありません。 。
コルーチン関数のスタックサイズの推定では、コルーチン関数内の他のすべてのAPIでローカル変数を呼び出すコストが、コルーチンに使用されるメモリに割り当てられ、呼び出しなどの予測できない変数がいくつかありますサードパーティAPI、サードパーティAPIには非常に大きな変数があります。mmapを使用して、実際の使用プロセスの最初にメモリを割り当て、割り当てられたメモリのmprotect保護のためにGUARD_PAGEを設定し、メモリオーバーフローに対して、場所を正確に決定し、割り当てが必要なスタックを適切に調整できます。サイズ。
ucontextクラスター関数learning_actual use.png
エオリアコルーチン
Fengshenコルーチンはucontextパッケージに基づいています
スケジュール
struct schedule {
char stack[STACK_SIZE]; // 原来schedule里面就已经存有了stack
ucontext_t main; // ucontext_t你可以看做是记录上下文信息的一个结构
int nco; // 协程的数目
int cap; // 容量
int running; // 正在运行的coroutine的id
struct coroutine **co; // 这里是一个二维的指针
};
コルーチン
struct coroutine {
coroutine_func func; // 运行的函数
void *ud; // 参数
ucontext_t ctx; // 用于记录上下文信息的一个结构
struct schedule * sch; // 指向schedule
ptrdiff_t cap; // 堆栈的容量
ptrdiff_t size; // 用于表示堆栈的大小
int status;
char *stack; // 指向栈地址么?
};
coroutine_new
int coroutine_new(struct schedule *S, coroutine_func func, void *ud)
コルーチンを作成します。コルーチンはスケジュールコルーチンリストに追加されます。funcは実行する関数で、udはfuncの関数です。スケジュールで作成されたスレッドの数を返します
coroutine_yield
void coroutine_yield(struct schedule * S)
スケジューラスケジュールで現在実行中のコルーチンを一時停止し、メイン機能に切り替えます。
coroutine_resume
void coroutine_resume(struct schedule * S, int id) {
スケジューラスケジュールのID番号でコルーチンの実行を再開します
coroutine_close
void coroutine_close(struct schedule *S)
スケジュール内のすべてのコルーチンを閉じる
著者:小良介の
リンクします。https://www.jianshu.com/p/2782f8c49b2aの
出典:ジェーンの本が
著者によって著作権で保護されています。営利目的の複製については、作者に連絡して承認を得てください。非営利目的の複製については、出典を明記してください。