web-dev-qa-db-ja.com

JVMサポートなしのJVM langsでコルーチンはどのように実装されますか?

この質問は、Javaプログラミング言語でコルーチンを実装する方法を説明する Loomの提案 を読んだ後に出てきました。

特にこの提案では、この機能を言語で実装するには追加のJVMサポートが必要になると述べています。

私が理解しているように、KotlinやScalaなどの機能セットの一部としてコルーチンを持つJVMにはすでにいくつかの言語があります。

それでは、この機能は追加サポートなしでどのように実装され、それなしで効率的に実装できますか?

49
marknorkin

tl; dr要約:

特にこの提案では、この機能を言語で実装するには追加のJVMサポートが必要になると述べています。

彼らが「必要」と言うとき、それらは「言語間でパフォーマンスがあり相互運用可能であるような方法で実装されるために必要」を意味します。

したがって、追加サポートなしでこの機能を実装する方法

多くの方法がありますが、それがどのように機能するかを理解するのが最も簡単ですが(実装するのが必ずしも最も簡単ではありません)、JVMの上に独自のセマンティクスで独自のVMを実装することです。 (notが実際に行われていることに注意してください。これはwhyができることに関する直観にすぎません。)

そして、それなしで効率的に実装できますか?

あんまり。

少し長い説明

Project Loomの1つの目標は、この抽象化purelyをライブラリとして導入することです。これには3つの利点があります。

  • Javaプログラミング言語を変更するよりも、新しいライブラリを導入する方がはるかに簡単です。
  • ライブラリはJVM上のすべての単一言語で記述されたプログラムですぐに使用できますが、Java言語機能はJavaプログラムでのみ使用できます。
  • 新しいJVM機能を使用しない同じAPIを備えたライブラリを実装できます。これにより、単純な再コンパイルで(パフォーマンスは低下しますが)古いJVMで実行されるコードを作成できます。

ただし、ライブラリとして実装すると、コンパイラが関与しないであるため、コルーチンを他の何かに変える巧妙なコンパイラトリックができなくなります。巧妙なコンパイラーのトリックがなければ、優れたパフォーマンスを得るのははるかに難しく、JVMサポートの「要件」です。

詳細な説明

一般に、通常の「強力な」制御構造はすべて計算の意味で同等であり、互いに使用して実装できます。

これらの「強力な」ユニバーサル制御フロー構造の中で最もよく知られているのは、由緒あるGOTOです。もう1つはContinuationです。次に、スレッドとコルーチンがあり、人々がよく考えないものがありますが、それはGOTO:Exceptionsと同等です。

別の可能性としては、コールスタックの再定義があります。そのため、コールスタックはオブジェクトとしてプログラマーにアクセス可能であり、変更および再作成できます。 (たとえば、多くのSmalltalk方言がこれを行います。また、これはCおよびAssemblyで行われる方法にも似ています。)

oneがあれば、allを1つだけ実装することで、それらを重ねることができます。

JVMには、例外とGOTOの2つがありますが、JVMのGOTOnot universalで、非常に制限されています。動作するのはinside単一のメソッド。 (本質的にはループのみを対象としています。)そのため、例外が発生します。

だから、それはあなたの質問に対する1つの可能な答えです:あなたは例外の上にコルーチンを実装することができます。

別の可能性は、JVMの制御フローat allを使用せず、独自のスタックを実装することです。

ただし、これは通常、JVMでコルーチンを実装するときに実際に使用されるパスではありません。ほとんどの場合、コルーチンを実装する人は、トランポリンを使用し、オブジェクトとして実行コンテキストを部分的に再定義することを選択します。つまり、たとえば、CLI上のC#でジェネレーターを実装する方法です(JVMではなく、課題は似ています)。 C#のジェネレーター(基本的には準コルーチン)は、メソッドのローカル変数をコンテキストオブジェクトのフィールドに持ち上げ、各yieldステートメントでメソッドをそのオブジェクトの複数のメソッドに分割することにより実装されます。それらをステートマシンに変換し、コンテキストオブジェクトのフィールドを介してすべての状態変更を慎重にスレッド化します。そして、async/awaitが言語機能として登場する前に、賢いプログラマーは同じ機械を使用して非同期プログラミングを実装しました。

[〜#〜] however [〜#〜]、そしてそれはあなたが指摘した記事が言及している可能性が高いことです。すべての機械は高価です。独自のスタックを実装するか、実行コンテキストを別のオブジェクトに持ち上げるか、すべてのメソッドを1つのgiantメソッドにコンパイルして、どこでもGOTOを使用する場合(メソッドのサイズ制限)、または制御フローとして例外を使用する場合、次の2つのうち少なくとも1つが当てはまります。

  • 呼び出し規約は、他の言語が期待するJVMスタックレイアウトと互換性がなくなります。つまり、相互運用性を失います。
  • JITコンパイラーは、コードが何をしているのかわからず、バイトコードパターン、実行フローパターン、および使用パターン(例:スローおよびキャッチginormous例外の量)で表示されます。最適化の方法を予期しておらず、わかりません。つまり、パフォーマンスを失います。

Rich Hickey(Clojureのデザイナー)はかつて講演で次のように語っています。「Tail Calls、Performance、Interop。PickTwo」私はこれを私が呼ぶものに一般化したHickey's Maxim: "Advanced Control-Flow、Performance、Interop。Pick Two。"

実際、一般的にone of interopまたはperformanceを達成することは困難です。

また、コンパイラーはより複雑になります。

JVMでコンストラクトがネイティブに使用可能になると、これらはすべてなくなります。たとえば、JVMにスレッドがなかった場合を想像してください。次に、すべての言語実装が独自のスレッドライブラリを作成します。これは、ハード、複雑、低速であり、other言語実装のスレッドライブラリと相互運用しません。

最近の実世界の例はラムダです。JVMの多くの言語実装にはラムダがありました。スカラ。次に、Javaもラムダを追加しましたが、JVMはラムダをサポートしていないため、encodedである必要があり、Oracleが選択したエンコーディングはScalaは以前選択したため、JavaラムダをScalaメソッドにScala Functionを期待して渡すことができませんでした。この場合の解決策は、Scala開発者がラムダのエンコーディングを完全に書き直して、Oracleが選択したエンコーディングと互換性を持たせることでした。実際、これはいくつかの場所で後方互換性を壊しました。

39
Jörg W Mittag

コルーチンに関するKotlinのドキュメント (エンファシス鉱山)から:

コルーチンは、複雑なものをライブラリに入れることで非同期プログラミングを簡素化します。プログラムのロジックはコルーチンで順番に表現でき、基礎となるライブラリーが非同期性を判断します。 ライブラリは、ユーザーコードの関連部分をコールバックにラップし、関連するイベントにサブスクライブし、異なるスレッドで実行をスケジュールすることができます(または異なるマシン!)コードは、連続して実行されたかのように単純なままです。

要するに、コールバックとステートマシンを使用して中断および再開を処理するコードにコンパイルされます。

プロジェクトリーダーのロマンエリザロフは、このテーマについてKotlinConf 2017で2つの素晴らしい講演を行いました。 1つは コルーチンの紹介 、2つ目は コルーチンの詳細 です。

22
Todd

コルーチンオペレーティングシステムまたはJVMの機能に依存しない。代わりに、コルーチンおよびsuspend関数は、コンパイラーによって変換され、一般的に一時停止を処理し、状態を維持して一時停止中のコルーチンを渡すことができる状態マシンを生成します。これは、Continuationsによって有効になります。これは、パラメータとして、すべての中断機能にパラメータとして追加されますコンパイラ;この手法は、「 Continuation-passing style 」(CPS)と呼ばれます。

suspend関数の変換で1つの例を見ることができます。

suspend fun <T> CompletableFuture<T>.await(): T

以下は、CPS変換後の署名を示しています。

fun <T> CompletableFuture<T>.await(continuation: Continuation<T>): Any?

ハードな詳細を知りたい場合は、これを読む必要があります 説明

4
s1m0nw1

Project Loom の前に、同じ著者による Quasar ライブラリがありました。

docs からの引用です:

内部的には、ファイバーはスケジューラーでスケジュールされる継続です。継続は、計算の瞬間的な状態をキャプチャし、一時停止してから、一時停止した時点から後で再開できるようにします。 Quasarは、サスペンド可能なメソッドを(バイトコードレベルで)インスツルメントすることで継続を作成します。スケジューリングには、QuasarはForkJoinPoolを使用します。これは非常に効率的で、作業を盗む、マルチスレッドスケジューラです。

クラスがロードされるたびに、Quasarのインストルメンテーションモジュール(通常はJavaエージェント)として実行されます)が一時停止可能なメソッドをスキャンします。すべての一時停止可能なメソッドfは、次の方法でインストルメントされます。一時停止可能なメソッドgの呼び出しごとに、ローカル変数の状態をファイバーのスタックに保存(および復元)するgの呼び出しの前(および後)にコードが挿入されます(ファイバーが独自のスタックを管理します) 、およびこれ(つまり、gへの呼び出し)が中断ポイントである可能性があるという事実を記録します。この「中断可能な関数チェーン」の最後に、Fiber.parkへの呼び出しがあります。例外(メソッドにcatch(Throwable t)ブロックが含まれている場合でも、インスツルメンテーションによってキャッチできないようにします)。

Gが実際にブロックされる場合、SuspendExecution例外はFiberクラスによってキャッチされます。ファイバーが(パーク解除で)ウェイクアップされると、メソッドfが呼び出され、実行レコードにgの呼び出しでブロックされていることが示されるため、gが呼び出されるfの行にすぐにジャンプします。そしてそれを呼び出します。最後に、実際の一時停止ポイント(パークへの呼び出し)に到達し、呼び出しの直後に実行を再開します。 gが戻ると、fに挿入されたコードはファイバースタックからfのローカル変数を復元します。

このプロセスは複雑に聞こえますが、パフォーマンスオーバーヘッドは3%〜5%以下です。

ほとんどすべての純粋なJava 継続ライブラリ は、スタックフレーム上のローカル変数をキャプチャおよび復元するために、同様のバイトコードインストルメンテーションアプローチを使用したようです。

KotlinとScalaコンパイラーは、 より分離された を実装するのに十分勇敢であり、他のいくつかで言及されたステートマシンに対して CPS変換 ここで答えます。

2
Vadzim