bdw-gc コルーチンサポート
Crystal は bdw-gc を使用し、コルーチンをサポートしています。ここでは、コルーチンはファイバーと呼ばれています。長年にわたり、Crystal はファイバーを使用したシングルスレッドでした。シングルスレッドは依然としてデフォルトの選択肢です。しばらく前に、各スレッドが複数のファイバーを並行して実行できるマルチスレッドサポートを追加しました。これには、ライブラリにコルーチンの組み込みサポートがなかったため、これを実現するために bdw-gc へのいくつかのパッチと最終的なコントリビューションが必要でした。
マルチスレッドコルーチンのサポートは、ユーザーが各スレッドのスタックボトムを制御できるようにすることで得られました。スタックボトムと命令ポインタを変更することが、コルーチンに実質的に命を与えるものです。つまり、OSに通知することなく、プログラムのどの部分を次に実行するかをプログラムが選択できるようにするのです。OSに通知することは、スレッドを使用することと同等であり、よりコストがかかります。
したがって、Crystal やその他の言語は、各スレッドで複数のコルーチンを同時に実行できるマルチスレッドプログラムから恩恵を受ける可能性があります。
コルーチンを実装する場合、ランタイムは、まだ実行を続ける必要のある既存のコルーチンの何らかの形の簿記処理を行う可能性が高いです。これらの記録には、スタック、命令ポインタ、およびランタイムに固有のその他の情報の中でレジスタの永続性が含まれます。
以下では、Crystal がシングルスレッドモードとマルチスレッドモードでどのように bdw-gc を使用してコルーチンサポートを実現するかを説明します。Crystal ランタイムとその簿記処理の詳細には焦点を当てません。これは主に bdw-gc とのインターフェースに焦点を当てています。
両方のシナリオで取り上げるトピックの側面は次のとおりです。
- 現在のコルーチンを別のコルーチンに切り替える必要がある場合はどうする必要がありますか?
- 実行されておらず、現在のスタックからアクセスできないものも含めて、すべてのコルーチンを認識するように bdw-gc はどのように設定されていますか?
シングルスレッドコルーチン
現在のコルーチン C_0
を別のコルーチン C_1
に切り替える必要がある場合、
- グローバル変数
GC_stackbottom
をstack_bottom(C_1)
に設定します C_0
とC_1
の間でコンテキストスイッチを行います
stack_bottom(C_1)
の値は、コルーチンが割り当てられたときにわかります。コルーチンの割り当ては、ほとんどの場合、そのコルーチンのスタックとして使用されるヒープ領域を予約することを意味します。したがって、スタックボトムはその時点でわかります。
エッジケースは、プログラムのメインスレッドに属する最初のコルーチンで何が起こるかです。プログラムの開始時にグローバル変数 GC_stackbottom
を使用することで、最初のファイバーのスタックボトムを取得できます。
シングルスレッドなので、すべてのコルーチンはランタイムによって作成されるか、メインスレッドがコルーチンとして見なされます。
コンテキストスイッチはどのように行うのですか? C_0
は、すべての重要なコンテキスト(これはアーキテクチャ固有です)を保持するルーチンへの通常の関数呼び出しを行います。次に、C_1
の保存されたコンテキストから、スタックポインタが復元され、戻りが行われます。事実上、C_0
を中断し、C_1
を再開しています。このプロセスの詳細については、src/fiber/context.cr にありますが、これは GC に依存しません。
bdw-gc のセットアップ部分に対応するために、GC がコレクションを試みる前にフックするために GC_set_push_other_roots
を使用します。この手順では、現在のコルーチンではない、つまり実行されていないコルーチンのすべてのスタックをプッシュします。
現在のコルーチンスタックは、GC_stackbottom
を介して GC によってすでに認識されており、残りはすべて GC_set_push_other_roots
を介して認識されます。GC がメインスレッドを一時停止してコレクションを実行するので、ケアする必要のあるすべてのメモリを適切に把握できます。素晴らしい!
マルチスレッドコルーチン
より単純なシングルスレッドがカバーされたので、マルチスレッドについて説明できます。
これまでのところ、シングルスレッドでは GC を無効にする必要はありませんでした。パフォーマンス上の理由から、この状態を維持する方が優れています。しかし、マルチスレッド環境では、ファイバーの切り替えルーチンを中心にロックが必要になります。グローバルな リーダー/ライターロック を使用します。
現在のコルーチン C_0
を別のコルーチン C_1
に切り替える必要がある場合、
C_1
は現在のスレッドで実行されることをマークします- グローバルロックにリーダーを追加します
C_0
とC_1
の間でコンテキストスイッチを行います- グローバルロックからリーダーを削除します
シングルスレッドの場合のように GC_stackbottom
にアクセスしていないことに注意してください。また、コンテキストスイッチは以前とまったく同じです。
最後のステップはコンテキストスイッチではないため、ファイバーの実行開始時にグローバルロックからリーダーを削除する必要があります。ランタイムによって作成されたファイバーでのみです。
このことについて考える1つの方法は、コンテキストスイッチの後、次のステップは C_0
ではなく C_1
で実行されるということです。C_1
が以前にコルーチンスイッチによって削除された場合、コードはそのままですが、最初に実行される場合は、プログラマーによって指示された命令の前に最後のステップを実行する必要があります。
bdw-gc のセットアップ部分に対応するために、GC がコレクションを試みる前にフックするために、引き続き GC_set_push_other_roots
を使用します。この手順では、実行されていないコルーチンのすべてのスタックをプッシュします。また、実行中のコルーチンに対処する必要があります。この場合、アプリケーションスレッドごとに1つあります(GC のスレッドも省略できるので、アプリケーションスレッドと呼びましょう)。
したがって、この手順の一部として、実行中のすべてのファイバーのスタックボトムも GC に通知します。このために、すべてのアプリケーションスレッドを反復処理し、反復処理されたスレッドの実行中のファイバーのスタックボトムを使用して GC_set_stackbottom
を呼び出します。
手順の最後のステップとして、グローバルロックからライターを削除します。
したがって、GC_set_push_other_roots
に登録された手順を要約すると、次のようになります。
- 実行されていないファイバーのすべてのスタックをプッシュします
GC_set_stackbottom
を介して、実行中の各ファイバー(アプリケーションスレッドごとに1つ)のスタックボトムを通知します- グローバルロックからライターを削除します
シングルスレッドのアナロジーは、各コンテキストスイッチに対して GC_set_stackbottom
を呼び出すことになりますが、GC_set_stackbottom
を呼び出すと GC ロックが取得されるため、必要な場合にのみ実行する方が優れています。おそらく、シングルスレッドケースはこれを模倣することができますが、歴史的な理由から、この違いが生じました。
グローバルロックにライターが追加される場所が欠落しています。これは、GC_set_start_callback
で登録されたコールバックで行われます。
グローバルロックの効果は、コレクションが進行中の場合を除き、同時コンテキストスイッチを許可することです。
シングルスレッドの場合と同様に、コルーチンには 2 種類あります。a) ランタイムによって手動で作成され、プログラムのヒープに存在するスタックを持つものと、b) スレッドの初期スタックに対応するものです。最初のスタックボトムを知ることは以前と同じで、メモリはわかっています。後者については、ファイバーがランタイムに登録されるときに GC_get_my_stackbottom
を使用します。
これが、GC_get_my_stackbottom
と GC_set_stackbottom
がマルチスレッド環境でコルーチンを有効にするために使用される方法です。これを可能にするために多くの要素が組み合わさっているため、これらがどのように使用できるかが明確になることを願っています。
ソースコード
カバーされていない詳細がいくつかありますが、これらは Crystal ランタイムに関するものです。ファイバーのスタックメモリのプールを保持して再利用する方法、スレッドセーフな連結リストでの実行中のファイバーとスレッドのリストなどです。詳細についてさらに動機付けが必要な場合は、関連するファイルは次のとおりです。