コルーチンは、中断および再開が可能なコンピュータプログラムの構成要素であり、協調的なマルチタスク処理のためにサブルーチンを一般化したものです。コルーチンは、協調タスク、例外処理、イベントループ、イテレータ、無限リスト、パイプなど、おなじみのプログラム構成要素の実装に最適です。
これらは「実行を一時停止できる関数」と表現されている。[ 1 ]
メルビン・コンウェイは1958年にアセンブリプログラムの構築に適用した際に「コルーチン」という用語を作り出した。[ 2 ]コルーチンの最初の公開された説明は、その後1963年に発表された。[ 3 ]
コルーチンには厳密な定義は存在しない。1980年にクリストファー・D・マーリン[ 4 ]は、コルーチンの広く認められている2つの基本的な特徴を要約した。
さらに、コルーチン実装には3つの特徴があります。
yield通常、やなどのキーワードを提供しますresume。プログラマは、どのフレームに制御を譲るかを自由に選択することはできません。ランタイムは、現在のコルーチンの最も近い呼び出し元にのみ制御を譲ります。一方、対称コルーチンでは、プログラマは制御の譲渡先を指定する必要があります。yield。2009 年に発表された論文「Revisiting Coroutines」[ 5 ]では、第一級コルーチンをサポートし、スタックフルであることを示す用語として「フルコルーチン」が提案されました。フルコルーチンは、ワンショット継続や限定継続と同じ表現力を持つため、独自の名称に値します。フルコルーチンは、対称型または非対称型のいずれかです。重要なのは、コルーチンが対称型か非対称型かは、その表現力には影響しないということです。ただし、フルコルーチンは非フルコルーチンよりも表現力に優れています。表現力は同じですが、非対称コルーチンは、制御が常に呼び出し元に渡されるという意味で、ルーチンベースの制御構造により近いものとなっており、プログラマにとってはより馴染み深いものかもしれません。
サブルーチンはコルーチンの特殊なケースです。[ 6 ]サブルーチンが呼び出されると、実行は開始点から始まり、サブルーチンが終了すると、その実行は終了します。サブルーチンのインスタンスは一度だけ戻り、呼び出し間で状態を保持しません。対照的に、コルーチンは他のコルーチンを呼び出すことで終了できます。呼び出したコルーチンは、後で元のコルーチンで呼び出された点に戻る可能性があります。コルーチンの観点からは、終了しているのではなく、別のコルーチンを呼び出していることになります。[ 6 ]したがって、コルーチンのインスタンスは状態を保持し、呼び出し間で変化します。特定のコルーチンの複数のインスタンスが同時に存在できます。別のコルーチンに「譲る」ことによって別のコルーチンを呼び出すことと、単に別のルーチンを呼び出すこと(この場合も、元の点に戻ります)との違いは、互いに譲り合う 2 つのコルーチン間の関係が呼び出し元と呼び出し先の関係ではなく、対称的であることです。
任意のサブルーチンは、 yieldを呼び出さないコルーチンに変換できます。[ 7 ]
コルーチンがどのように役立つかを示す簡単な例を以下に示します。あるルーチンがアイテムを作成してキューに追加し、別のルーチンがキューからアイテムを取り出して使用するという、コンシューマー・プロデューサー関係があるとします。効率上の理由から、複数のアイテムを一度に追加および削除したいとします。コードは次のようになるでしょう。
q := new Queue < Item > コルーチンは、qが満杯でない間ループ を生成します 。 新しいアイテムを作成する アイテムをqに追加する 消費に充てるコルーチン消費 ループ( qが空でない間) qからいくつかの項目を削除する アイテムを使用する 収穫して生産する コールプロデュース
その後、 yieldコマンドを使用してキューを完全に満たすか空にしてから、他のコルーチンに制御を譲ります。以降のコルーチン呼び出しは、 yieldコマンドの直後、外側のコルーチンループ内で開始されます。
この例はマルチスレッドの入門としてよく用いられますが、実際には2つのスレッドは必要ありません。yield文は、一方のルーチンからもう一方のルーチンへ直接ジャンプすることで実装できます。
コルーチンはスレッドと非常によく似ています。ただし、コルーチンは協調型マルチタスクであるのに対し、スレッドは通常プリエンプティブ型マルチタスクです。コルーチンは、タスクを順不同または変更可能な順序で実行しても全体の結果が変わらないため、並行性を提供しますが、複数のタスクを同時に実行しないため、並列性は提供しません。コルーチンがスレッドよりも優れている点は、ハードリアルタイム環境で使用できること(コルーチン間の切り替えにシステムコールやブロッキングコールが一切含まれない)、クリティカルセクションを保護するためにミューテックスやセマフォなどの同期プリミティブが不要であること、オペレーティングシステムからのサポートが不要であることです。
プリエンプティブにスケジュールされたスレッドを使用してコルーチンを実装することは可能であり、呼び出し元のコードからは透過的になりますが、いくつかの利点(特にハードリアルタイム動作への適合性や、スレッド間の切り替えの相対的なコストの低さ)が失われます。
ジェネレータ(セミコルーチンとも呼ばれる)[ 8 ]は、コルーチンのサブセットです。具体的には、どちらも複数回yieldして実行を中断し、複数のエントリポイントで再入することができますが、コルーチンはyield直後に実行がどこで再開されるかを制御できるのに対し、ジェネレータは制御できず、代わりにジェネレータの呼び出し元に制御を戻します。[ 9 ]つまり、ジェネレータは主にイテレータの記述を簡略化するために使用されるため、yieldジェネレータ内のステートメントはジャンプ先のコルーチンを指定するのではなく、親ルーチンに値を渡します。
しかし、ジェネレータ機能の上にコルーチンを実装することは依然として可能であり、そのためには、ジェネレータから返されるトークンによって識別される子ジェネレータに明示的に制御を渡すトップレベルのディスパッチャルーチン(実質的にはトランポリン)を利用する必要がある。
q := new Queue < Item > ジェネレーターは、 qが満杯でない間ループ を生成する 。 新しいアイテムを作成する アイテムをqに追加する 収率ジェネレーターはqが空でない間ループ を消費する qからいくつかの項目を削除する アイテムを使用する 収率サブルーチンディスパッチャ d := new Map < Generator , Iterator > d[produce] :=消費開始 d[consume] :=生産開始 現在の := 生成する ループ呼び出し現在 current := next d[current] ディスパッチャーに電話する
ジェネレーターをサポートするがネイティブコルーチンを持たない言語(例えば、Python [ 10 ] 2.5 以前)向けのコルーチンの実装の多くは、このモデルまたは類似のモデルを使用しています。
ステートマシンや並行処理にコルーチンを使用することは、末尾呼び出しによる相互再帰の使用と似ています。どちらの場合も、制御は一連のルーチンの別のルーチンに切り替わります。しかし、コルーチンの方が柔軟性が高く、一般的に効率的です。コルーチンは戻り値を返すのではなく、実行を再開し、最初からやり直すのではなく、状態を保持することができます。つまり、変数(クロージャの場合と同様)と実行位置の両方を保持できます。また、yieldは末尾位置に限定されません。相互再帰サブルーチンでは、共有変数を使用するか、状態をパラメータとして渡す必要があります。さらに、サブルーチンの相互再帰呼び出しごとに新しいスタックフレームが必要になります(末尾呼び出しの排除が実装されている場合を除く)。一方、コルーチン間で制御を渡す場合は、既存のコンテキストを使用するため、ジャンプするだけで簡単に実装できます。
コルーチンは、以下のことを実装するのに役立ちます。
コルーチンは元々アセンブリ言語の手法として生まれたものですが、一部の高級プログラミング言語でもサポートされています。
Javaにはコルーチンをネイティブまたはライブラリでサポートする機能はありませんが、Kotlinのコルーチンを呼び出すことは可能ですkotlinx.coroutines(ただし、これは理想的ではなく、Kotlinの上にJavaラッパーを作成する必要があります)。
継続はコルーチンを実装するために使用できるため、継続をサポートするプログラミング言語は、コルーチンも比較的容易にサポートできる。
2003年現在C言語とその派生言語を含む、最も人気のあるプログラミング言語の多くは、言語自体や標準ライブラリにコルーチンのサポートを組み込んでいません。これは主に、スタックベースのサブルーチン実装の制限によるものです。例外として、 Boostライブラリの一部であるC++ライブラリBoost.Contextがあります。これは、POSIX、Mac OS X、Windows上で、ARM、MIPS、PowerPC、SPARC、x86のコンテキストスワッピングをサポートしています。Boost.Contextを基盤としてコルーチンを構築できます。
コルーチンが本来の実装方法であるにもかかわらず利用できない場合、一般的な対応策はクロージャを使用することです。クロージャとは、状態変数(静的変数、多くの場合ブール型フラグ)を持つサブルーチンで、呼び出し間で内部状態を維持し、適切な箇所に制御を移します。コード内の条件分岐によって、状態変数の値に基づいて、連続する呼び出しで異なるコードパスが実行されます。もう一つの一般的な対応策は、大規模で複雑なswitch文、またはgoto文(特に計算されたgoto文)の形で明示的な状態機械を実装することです。このような実装は理解や保守が困難であると考えられており、コルーチンサポートの必要性を示唆しています。
スレッド、そして程度は低いもののファイバーは、今日の主流プログラミング環境においてコルーチンの代替手段として用いられています。スレッドは、同時に実行される複数のコードのリアルタイムな協調的相互作用を管理するための機能を提供します。スレッドはC言語をサポートする環境で広く利用可能であり(他の多くの現代的な言語でもネイティブにサポートされています)、多くのプログラマーにとって馴染み深く、通常は適切に実装され、ドキュメントも充実しており、サポートも手厚く提供されています。しかし、スレッドは大規模で複雑な問題を解決するため、多くの強力かつ複雑な機能を備えており、それに伴い習得も容易ではありません。そのため、コルーチンだけで十分な場合には、スレッドを使用するのは過剰となる可能性があります。
スレッドとコルーチンの重要な違いの一つは、スレッドは通常プリエンプティブにスケジュールされるのに対し、コルーチンはそうではないという点です。スレッドはいつでも再スケジュールでき、並行して実行できるため、スレッドを使用するプログラムはロックに注意する必要があります。一方、コルーチンはプログラムの特定の時点でのみ再スケジュールでき、並行して実行されないため、コルーチンを使用するプログラムは多くの場合、ロックを完全に回避できます。この特性は、イベント駆動型プログラミングや非同期プログラミングの利点としても挙げられます。
ファイバーは協調的にスケジューリングされるため、上記のコルーチンを実装するための理想的な基盤となります。[ 23 ]しかし、ファイバーに対するシステムサポートは、スレッドに対するサポートに比べて不足していることがよくあります。
マシン依存のアセンブリ言語では、コルーチンを直接実行するためのメソッドが用意されていることが多い。例えば、PDP-11ファミリーのミニコンピュータのアセンブリ言語であるMACRO-11では、「古典的な」コルーチン切り替えは「JSR PC,@(SP)+」という命令によって行われる。この命令はスタックからポップされたアドレスにジャンプし、現在の(つまり次の)命令アドレスをスタックにプッシュする。VAXen ( VAX MACRO)では、これに相当する命令は「JSB @(SP)+」である。Motorola 6809にも「JSR [,S++]」という命令がある。「++」は、スタックから2バイト(アドレス)がポップされることを示している。この命令は、(標準の)「モニタ」Assist 09でよく使われる。
汎用コルーチンを実装するには、2 つ目のコールスタックを取得する必要がありますが、これはC言語では直接サポートされていない機能です。これを実現する信頼性の高い (ただしプラットフォーム固有の) 方法は、少量のインラインアセンブリを使用して、コルーチンの初期作成時にスタックポインタを明示的に操作することです。これは、Protothreadsで使用されている方法との相対的な利点についての議論でTom Duffが推奨しているアプローチです。[ 24 ] POSIX sigaltstackシステムコールを提供するプラットフォームでは、シグナルハンドラ内から springboard 関数を呼び出すことで 2 つ目のコールスタックを取得できます[ 25 ] [ 26 ]。これにより、多少の複雑さが増すものの、移植性の高いC 言語で同じ目的を達成できます。POSIXまたはSingle Unix Specification (SUSv3) に準拠する C ライブラリは、getcontext、setcontext、makecontext、swapcontextなどのルーチンを提供していましたが、これらの関数は POSIX 1.2008 で廃止されました。[ 27 ]
上記の方法のいずれかで2番目のコールスタックを取得したら、標準Cライブラリのsetjmp関数とlongjmp関数を使用して、コルーチン間の切り替えを実装できます。これらの関数は、ABIで要求されるスタックポインタ、プログラムカウンタ、呼び出し先保存レジスタ、およびその他の内部状態をそれぞれ保存および復元します。これにより、yield後にコルーチンに戻ると、関数呼び出しから戻るときに復元されるすべての状態が復元されます。setjmp関数とlongjmp関数を利用しない最小限の実装では、スタックポインタとプログラムカウンタのみを交換し、他のすべてのレジスタを上書きする小さなインラインアセンブリブロックを使用して同じ結果を得ることができます。setjmpとlongjmpはABIに従って使用されている可能性のあるすべてのレジスタを保守的に保存する必要がありますが、clobberメソッドではコンパイラが実際に使用されているとわかっているものだけを(スタックにスピルして)保存できるため、この方法の方がはるかに高速です。
直接的な言語サポートがないため、多くの著者が上記の詳細を隠蔽する独自のコルーチンライブラリを作成しています。Russ Cox の libtask ライブラリ[ 28 ]はこの種の良い例です。ネイティブ C ライブラリによってコンテキスト関数が提供されている場合はそれを使用し、そうでない場合は ARM、PowerPC、Sparc、および x86 用の独自の実装を提供します。その他の注目すべき実装には、libpcl [ 29 ]、coro [ 30 ]、lthread [ 31 ]、libCoroutine [ 32 ]、libconcurrency [ 33 ]、libcoro [ 34 ] 、 ribs2 [ 35 ]、libdill. [ 36 ]、libaco [ 37 ]、libco. [ 26 ]などがあります。
上記の一般的なアプローチに加えて、サブルーチンとマクロの組み合わせで C 言語のコルーチンを近似する試みがいくつか行われてきました。サイモン・タサムの貢献[ 38 ]は、ダフのデバイスに基づいており、このジャンルの注目すべき例であり、 Protothreadsや同様の実装の基礎となっています。[ 39 ]ダフの反対意見[ 24 ]に加えて、タサム自身のコメントは、このアプローチの限界について率直な評価を提供しています。「私の知る限り、これは真面目な本番コードでこれまで見られた中で最悪の C ハックです。」[ 38 ]この近似の主な欠点は、各コルーチンに個別のスタック フレームを維持しないため、関数からの yield をまたいでローカル変数が保持されず、関数への複数のエントリを持つことができず、制御はトップ レベルのルーチンからのみ yield できることです。[ 24 ]
C++20 では、実行の途中で中断して後で再開できるスタックレス関数として標準化されたコルーチンが導入されました。コルーチンの中断状態はヒープに格納されます。[ 40 ]この標準の実装は進行中であり、G++ および MSVC コンパイラは最新バージョンで標準コルーチンを完全にサポートしています。[ 41 ]同期および遅延評価範囲のジェネレータクラス がC ++std::generator<Ref, V, Alloc> 23 で追加されました。[ 42 ]適切なタスククラス がC++26std::execution::task<T, Env>で導入されました。[ 43 ]は できますが、コルーチン以外では を使用して呼び出され、 は を返します。[ 44 ]co_awaitstd::this_thread::sync_wait()std::optional<std::tuple<Ts...>>
これは、C++26クラスを使用したC++20コルーチンの例ですstd::execution::task<T, Env>。
import std ;using std :: optional ; using std :: tuple ; using std :: execution :: task ; using namespace std :: literals ;task < void > simulateWork () { std :: println ( "作業を実行中..." ); std :: this_thread :: sleep_for ( 500 ms ); std :: println ( "作業完了!" ); }task < int > add ( int a , int b ) noexcept { co_await simulateWork (); co_return a + b ; }task < int > test () { int ret = co_await add ( 1 , 2 ); std :: println ( "Return {}" , ret ); co_return ret ; }int main ( int argc , char * argv []) { optional < tuple < int >> result = std :: this_thread :: sync_wait ( test ()); std :: println ( "Result: {}" , std :: get < 0 > ( result ). value_or ( std :: make_tuple ( -1 )));// 空のタプルに、正常完了を確認するための番兵識別子を割り当てます。optional < tuple <>> result = std :: this_thread :: sync_wait ( simulateWork ()); if ( result . has_value ()) { std :: println ( "作業が正常に完了しました。" ); } else { std :: println ( "作業が失敗しました!" ); }return 0 ; }コルーチンの場合、プログラマーは戻り値の型に対してawait_ready()、、await_suspend()などのパブリックメンバ関数を実装する必要があります。await_resume()
C# 2.0 では、イテレータ パターンとキーワードを通じてセミ コルーチン (ジェネレータyield) 機能が追加されました。[ 45 ] [ 46 ] C# 5.0では、 await構文のサポートが含まれています。
Cloroutineは、 Clojureでスタックレスコルーチンをサポートするサードパーティライブラリです。マクロとして実装されており、任意のvar呼び出しに基づいて任意のコードブロックを静的に分割し、状態を持つ関数としてコルーチンを出力します。
D はファイバーcore.thread.Fiberの標準ライブラリクラスとしてコルーチンを実装しています。[ 47 ]ジェネレータ ( ) [ 48 ]により、ファイバー関数を入力範囲 ( ) [ 49 ]として公開することが容易になり、既存の範囲アルゴリズムと互換性のあるファイバーが実現します。std.concurrency.Generatorstd.range.interfaces.InputRange
Go には「ゴルーチン」という組み込みの概念があり、これは Go ランタイムによって管理される軽量で独立したプロセスである一種のグリーン スレッドです。新しいゴルーチンは「go」キーワードを使用して開始できます。各ゴルーチンは、必要に応じて拡張できる可変サイズのスタックを持っています。ゴルーチンは通常、Go の組み込みチャネルを使用して通信します。[ 50 ] [ 51 ] [ 52 ] [ 53 ]ただし、ゴルーチンはコルーチンではありません (たとえば、ローカル データは連続する呼び出し間で保持されません)。[ 54 ]
Javaにはコルーチンの実装がいくつかあります。Javaの抽象化によって課せられる制約にもかかわらず、JVMは可能性を排除していません。[ 55 ]一般的に使用される方法は4つありますが、そのうち2つは標準に準拠したJVM間でのバイトコードの移植性を損ないます。
suspend」ができないため、代わりにを使用してブロックするか、またはAPIをkotlinx.coroutines.runBlocking公開するか、または(最も慣用的な方法として)を返す必要があります。kotlinx.coroutines.CoroutineScopekotlinx.coroutines.Jobjava.util.concurrent.CompletableFutureECMAScript 2015以降、JavaScriptはジェネレーターをサポートしています。ジェネレーターはコルーチンの特殊なケースです。[ 57 ]
Kotlinは、コルーチンを自社開発ライブラリの一部として実装しています。
import kotlinx.coroutines.*fun main () = runBlocking { launch { delay ( 1000L ) print ( "コルーチン内からこんにちは!" ) }print ( "Hello world!" ) }Luaはバージョン 5.0 (2003 年) 以降、標準ライブラリのコルーチンで第一級のスタックフル非対称コルーチンをサポートしています。[ 58 ] [ 59 ] [ 60 ]
Wirthによって定義されたModula-2は、標準SYSTEMライブラリの一部としてコルーチンを実装しています。
手続き NEWPROCESS() は、コードブロックとスタック領域をパラメータとして指定してコンテキストを設定し、手続き TRANSFER() は、コルーチンのコンテキストをパラメータとして指定して、コルーチンに制御を移します。
.NET Framework 2.0の開発中、Microsoft は、SQL Server のファイバーモードでの使用を念頭に、ファイバーベースのスケジューリングを処理するために、共通言語ランタイム(CLR) ホスティング APIの設計を拡張しました。 [ 62 ]ICLRTask::SwitchOutリリース前に、時間的な制約により、タスク切り替えフックのサポートが削除されました。 [ 63 ]その結果、ファイバー API を使用してタスクを切り替えることは、現在 .NET Framework では実行可能なオプションではありません。
OCaml はThreadモジュールを通じてコルーチンをサポートしています。[ 64 ] これらのコルーチンは並列処理なしで並行処理を提供し、単一のオペレーティングシステムスレッド上でプリエンプティブにスケジュールされます。OCaml 5.0 以降では、グリーン スレッドも利用可能で、別のモジュールによって提供されます。
Python は関数を使用してコルーチンをサポートしていますasyncio.create_task()。[ 66 ]
import asyncio import time from asyncio import Taskasync def main () -> None : task1 : Task [ str ] = asyncio . create_task ( say_after ( 1 , "hello" )) task2 : Task [ str ] = asyncio . create_task ( say_after ( 2 , "world" ))print ( f " { time.strftime ( ' % X ' ) }に開始しました" )# 両方のタスクが完了するまで待機します(約2秒かかります)await task1 await task2print ( f " { time.strftime ( ' % X ' ) }で終了しました" )Racketはネイティブ継続機能を提供しており、公式パッケージカタログにはコルーチンの簡単な実装が含まれています。実装はS. De Gabrielleによるものです。
Schemeは継続を完全にサポートしているため、コルーチンの実装は非常に簡単で、継続のキューを維持するだけで済みます。
ほとんどのSmalltalk環境では、実行スタックは第一級オブジェクトであるため、追加のライブラリやVMサポートなしでコルーチンを実装できます。
バージョン8.6以降、Tclはコア言語でコルーチンをサポートしています。 [ 69 ]
Valaはコルーチンをネイティブにサポートしています。コルーチンはGTKのメインループと組み合わせて使用するように設計されていますが、少なくとも1回のyieldを実行する前に終了コールバックが呼び出されないように注意すれば、単独で使用することも可能です。
。 6. 対称性は複雑さを軽減する概念である(コルーチンにはサブルーチンが含まれる)。あらゆる場所でそれを探し求めよ。