コトリン(ハイド)のコルーチン







Simon Wirtzは彼のブログで、Kotlinに関する多くの興味深い投稿を投稿しています。

そのうちの一つの翻訳をお見せします。


紹介と動機



数日前にツイッターで述べたように、私はコーリンのコルーチンを詳しく調べるつもりでした。 しかし、残念ながら、予想よりも時間がかかりました。 ほとんどの場合、これは、特にルーチンの概念に精通していない場合、コルーチンが非常に膨大なトピックであるという事実によるものです。 いずれにせよ、私はあなたと私の意見を共有したいと思います、そして、私はあなたに徹底的なレビューをしたいと思います。







JetBrains ブログで述べられているように、コルーチンは間違いなく「大きな機能」の1つです。







負荷がかかるとブロックするのが悪いこと、 GOがほぼすべての場所でサンプルとして使用されていること、世界がますます非同期になり、通知処理に基づいていることがわかっています。 多くの言語(2012年のC#以降 )は、 async/await



などの構成要素を使用して、すぐに非同期プログラミングをサポートします。 Kotlinでは、このような構造はライブラリで宣言でき、非同期はキーワードではなく、単純な関数になると想定していました。 この設計により、 future/promises



callback-passing



など、さまざまな非同期APIを実装できます。 遅延ジェネレーター( yield



)およびその他の機能を実装するために必要なものもすべてあります。

つまり、マルチスレッドプログラミングを簡単に実装するためのコルーチンが導入されています。 確かにあなたの多くは、 Java 、そのThreadクラス、およびマルチスレッドプログラミングのクラスを扱っています。 私自身も彼らと多くの仕事をしてきており、彼らの決定の成熟度を確信しています。







JavaマルチスレッドとKotlinコルーチン



それでもJavaのスレッドとマルチスレッドで問題が発生する場合は、 Java同時実行性の練習帳をお勧めします







もちろん、エンジニアリングの観点から、 Javaからの実装は適切に設計されていますが、日常の作業で使用することは難しく、非常に冗長です。 さらに、ノンブロッキングプログラミング用のJava実装は多くありません。 多くの場合、スレッドを開始するときに、ブロックコード(ロック、期待など)にすぐにアクセスすることを完全に忘れるという事実に気付くことができます。代替の非ブロッキングアプローチは 、日常の作業では使いにくく、間違いを犯しやすいです。







一方、コルーチンは単純なシーケンシャルコードのように見え、ライブラリ内のすべての複雑さを隠します。 同時に、ロックなしで非同期コードを実行する機能を提供します。これにより、さまざまなアプリケーションに大きなチャンスが生まれます。 スレッドをブロックする代わりに、計算が中断されます。 JetBrainsは、コルーチンをJavaで知っているスレッドではなく、「軽量スレッド」として説明しています 。 コルーチンは非常に安価に作成でき、フローと比較したオーバーヘッドは比較できません。 後で見るように、コルーチンはライブラリの制御下でスレッドで実行されます。 別の重要な違いは制限です。 スレッドの数は、実際にはネイティブスレッドに対応しているため、制限されています。 一方、コルーチンの作成は事実上無料であり、数千個のコルーチンも簡単に起動できます。







マルチスレッドスタイル



異なる言語では、 callback



(JavaScript)、 future/promise



(Java、JavaScript)、 async/await



アプローチ(C#)などに基づいて、マルチスレッドプログラミングへのさまざまなアプローチを見つけることができます。 それらはすべて、プログラミングスタイルを課さないという事実により、コルーチンを使用して実装できます。 それどころか、どのスタイルも既に実装されているか、それらの助けを借りて実装することができます。







利点の1つとして、 callback



ベースのアプローチと比較して、コルーチンを使用すると、非同期コードを実装できます。これは、シーケンシャルコードのように見えます。 また、コルーチンは複数のスレッドで実行できますが、コードの一貫性は維持されるため、理解しやすくなります。







コルチンのコンセプト



これは、「コルーチン」の概念が新しいということではありません。 ウィキペディアの記事によると、名前自体は1958年にすでに知られていました。 C#GoPythonRubyなど、多くの最新のプログラミング言語がネイティブサポートを提供します。 Kotlinを含むコルチンの実装は、コンピュータープログラムの制御状態の抽象的な表現であるいわゆる「 継続 」に基づいていることがよくあります。 それらどのように機能するか(コルーチンの実装)に戻ります。







スタート。 基本



kotlinlang.orgには 、コルーチンを使用するようにプロジェクトをセットアップする方法を説明する包括的な資料があります。 前のリンクの資料を詳しく見るか、 GitHubのリポジトリからコードをベースとしてください。







コルチン成分



すでに述べたように、コルーチンライブラリは、理解しやすい高レベルのAPIを提供し、すぐに開始できるようにします。 学習する唯一の修飾子は中断です。 メソッドの追加修飾子として使用して、それらを割り込み可能としてマークします。







少し後、 APIからのいくつかの簡単な例を見てみましょうが、今のところは、まずは、 suspend



機能とは何かを詳しく見てみましょう。







割り込み可能な機能



コルーチンは、機能が中断される可能性があることを示すために使用されるsuspend



キーワードに基づいています。 つまり、このような関数の呼び出しはいつでも中断できます。 そのような関数はcorutinからのみ呼び出すことができ、corutinには少なくとも1つの実行関数が必要です。







 suspend fun myMethod(p: String): Boolean { //... }
      
      





上記の例からわかるように、割り込み関数は追加の修飾子を持つ通常の関数のように見えます。 そのような関数はコルーチンからしか呼び出せないことに注意してください。そうしないと、コンパイルエラーが発生します。







コルーチンは、通常の関数のシーケンス、または実行後に使用可能なオプションの結果を持つ割り込み関数のいずれかです。







例に移りましょう。



何とか何とか何とかして、特定の例に移りましょう。 基本から始めましょう:







 fun main(args: Array<String>) = runBlocking { //(1) val job = launch(CommonPool) { //(2) val result = suspendingFunction() //(3) println("$result") } println("The result: ") job.join() //(4) } >> prints "The result: 5"
      
      





この例では、2つの関数(1) runBlocking



と(2) launch



がコルーチンビルダーの使用例です。 多数の異なるビルドがあり、それぞれがさまざまな目的のコルーチンを作成します: launch



(作成して忘れる)、 async



promise



返す)、 runBlocking



(フローをブロックする)など。







(2) launch



作成された内部コルーチンがすべての作業を行います。 中断された関数(3)の呼び出しはいつでも中断でき、結果は計算後に表示されます。 メインスレッドでは、corutinを開始した後、コルーチンが完了する前にString



値が表示されます。 launchから起動されたCorutinは、すぐにJob



返しJob



。これは、実行をキャンセルしたり、(4) join()



を使用して計算を待機したりするために使用できます。 また、 join()



呼び出しは中断される可能性があるため、 runBlocking



がよく使用される別のコルーチンにラップする必要があります。 このビルダー(1)は、割り込みのスタイルで記述されたコードが通常のブロック形式で呼び出される機会を提供するために特別に作成されました( APIからの注意)。 join



(4)を削除join



と、コルーチンが結果の値を表示する前にプログラムが終了します。







ウサギの穴をもっと深く見ましょう



より現実に近い例を考えてみましょう。 アプリケーションでメールを送信する必要があると想像してください。 受信者の要求とメッセージテキストの生成にはかなりの時間がかかります。 両方のプロセスは独立しているため、それらを並行して実行できます。







 suspend fun sendEmail(r: String, msg: String): Boolean { //(6) delay(2000) println("Sent '$msg' to $r") return true } suspend fun getReceiverAddressFromDatabase(): String { //(4) delay(1000) return "coroutine@kotlin.org" } suspend fun sendEmailSuspending(): Boolean { val msg = async(CommonPool) { //(3) delay(500) "The message content" } val recipient = async(CommonPool) { //(5) getReceiverAddressFromDatabase() } println("Waiting for email data") val sendStatus = async(CommonPool) { sendEmail(recipient.await(), msg.await()) //(7) } return sendStatus.await() //(8) } fun main(args: Array<String>) = runBlocking(CommonPool) { //(1) val job = launch(CommonPool) { sendEmailSuspending() //(2) println("Email sent successfully.") } job.join() //(9) println("Finished") }
      
      





作成者のコードはわずかに単純化できます


sendEmailSuspendingおよびmainのコード
 suspend fun sendEmailSuspending(): Boolean { val msg = async(CommonPool) { delay(500) "The message content" } val recipient = async(CommonPool) { getReceiverAddressFromDatabase() } println("Waiting for email data") return sendEmail(recipient.await(), msg.await()) } fun main(args: Array<String>) = runBlocking(CommonPool) { sendEmailSuspending() println("Email sent successfully.") println("Finished") }
      
      





最初に、前の例で見たように、(1) runBlocking



内のlaunch



ビルダーを使用するため、(9)コルーチンが完了するのを待つことができます。 (2)では、割り込み関数sendEmailSuspending



を呼び出します。 このメソッド内では、2つの並列タスクを使用します。(3)メッセージテキストを取得し、(4) getReceiverAddressFromDatabase



を呼び出してアドレスを取得します。 両方のタスクは、 async



を使用して並列コルーチンで実行されます。 また、遅延呼び出しがブロックされていないことに注意する価値があります;コルーチンの実行を中断するために使用されます。







非同期ビルダー



このビルダーは、コンセプトが本当にシンプルです。 他の言語では、彼はpromiseを返します。これは厳密に言えば、 Kotlinでは Deferred



型として表されます。 promise



future



deferred



delay



などのこれらのエンティティはすべて通常交換可能であり、同じものの説明です。 計算の結果を返すことを「約束」し、いつでも待機できる非同期オブジェクト。







(7)で、 KotlinからのDeferred



オブジェクトの「待機」部分を既に見てきました。ここでは、両方のメソッドを待機した結果で割り込み関数が呼び出されました。 await()



メソッドは、 Deferredオブジェクトのインスタンスで呼び出され、その呼び出しは結果が利用可能になるまで中断されます。 sendEmail



呼び出しも非同期ビルダーでラップされるため、実行を待機できます。







見逃したもの:CoroutineContext



上記の例の重要な部分は、 CoroutineContext



クラスのインスタンスであるビルダー関数に渡される最初のパラメーターです。 このコンテキストは、コルーチンに渡すものであり、現在のJob



へのアクセスを提供するものです。







現在のコンテキストを使用して内部コルーチンを開始できます。その結果、子Job



は外部コルーチンの子孫になります。 これにより、親Job



への1回の呼び出しでコルーチン階層全体を取り消すことができます。

CoroutineContext



は、 CoroutineContext



さまざまなタイプのElement



が含まれています。







上記のすべての例では、 CommonPool



使用さdispatcher



。これはまさにdispatcher



です。 彼は、フレームワークの制御下でスレッドプールでコルーチンが実行されることを保証する責任があります。 別の方法として、限定されたストリーム、または特別に作成されたストリームを使用するか、独自のプールを使用できます。 コンテキストは、+演算子を使用して簡単に結合できます。







 launch(CommonPool + CoroutineName("mycoroutine")){...}
      
      





一般的な可変状態



あなたはおそらくそれについてすでに考えていました:もちろん、コルーチンはよく見えますが、どのように同期し、どのように異なるコルーチン間でデータを交換しますか?







さて、これはまさに私が最近尋ねた質問であり、これはスレッドプールを使用するほとんどのコルーチンにとって妥当な質問です。 同期には、スレッドセーフなデータ構造、1つのスレッドでの実行制限、ロックの使用など、さまざまな手法を使用できます(詳細については、 Mutex



Mutex



)。

一般的なパターンに加えて、 Kotlinコルーチンは「通信による交換」スタイルの使用を推奨します( QAを参照)。







特に、俳優はコミュニケーションに適しています。 それからメッセージを送受信できるコルーチンで使用できます。 例を見てみましょう:







俳優



 sealed class CounterMsg { object IncCounter : CounterMsg() // one-way message to increment counter class GetCounter(val response: SendChannel<Int>) : CounterMsg() // a request with channel for reply. } fun counterActor() = actor<CounterMsg>(CommonPool) { //(1) var counter = 0 //(9) actor state, not shared for (msg in channel) { // handle incoming messages when (msg) { is CounterMsg.IncCounter -> counter++ //(4) is CounterMsg.GetCounter -> msg.response.send(counter) //(3) } } } suspend fun getCurrentCount(counter: ActorJob<CounterMsg>): Int { //(8) val response = Channel<Int>() //(2) counter.send(CounterMsg.GetCounter(response)) val receive = response.receive() println("Counter = $receive") return receive } fun main(args: Array<String>) = runBlocking<Unit> { val counter = counterActor() launch(CommonPool) { //(5) while(getCurrentCount(counter) < 100){ delay(100) println("sending IncCounter message") counter.send(CounterMsg.IncCounter) //(7) } } launch(CommonPool) { //(6) while ( getCurrentCount(counter) < 100) { delay(200) } }.join() counter.close() // shutdown the actor }
      
      





上記の例では、 Actor



を使用しActor



。これは、それ自体がコルーチンであり、任意のコンテキストで使用できます。 アクターには、 counter



含まれているアプリケーションの現在の状態が含まれています。 ここでまた別の興味深い機能(2) Channel



会います







チャンネル



チャネルは、値のストリームを交換する機能を提供します。これは、ロックなしでJavaBlockingQueue



(プロデューサー/コンシューマーパターンを実装)を使用する方法に非常に似ています。 さらに、 send



receive



は割り込み可能な機能であり、 FIFO戦略を実装するチャネルを介してメッセージを送受信するために使用されます。







デフォルトでは、アクターにはそのようなチャネルが含まれており、他のコルーチンで使用してメッセージを送信できます。 上記の例では、アクターは自身のチャンネルからのメッセージを繰り返し処理し(ここでは割り込み呼び出しfor



動作します)、タイプに応じて処理します。 GetCounter



からSendChannel



独立した値を送信する形式で。







メソッド(5) main



の最初のコルーチンは便宜上のものであり、カウンター値が100未満になるまでメッセージ(7) IncCounter



をアクターに送信しますIncCounter



番目(6)はカウンター値が100未満になるまで待機します。 (8) getCurrentCounter



。これにGetCounter



メッセージがGetCounter



れ、応答でreceive



を待機している間に中断さGetCounter



ます。







ご覧のとおり、州全体が特定のアクターに隔離されています。 これにより、一般的な可変状態の問題が解決されます。







その他の機能と例



コルーチンをより深く掘り下げて作業したい場合は、 Kotlinのドキュメントをより詳細に読んで、特に素晴らしいガイドを見ることをお勧めします







仕組み-Corutinの実装



投稿を過負荷にしないために、詳細に深く入りすぎないようにします。 さらに、今後数週間のうちに、バイトコード生成の例とともに、より詳細な実装情報を含む続編を書く予定です。 そのため、ここでは「指で」簡単な説明に限定します。







コルーチンは、 JVMの機能にもオペレーティングシステムの機能にも基づいていません。 代わりに、コルーチンおよび割り込み可能関数は、コンパイラーによって、状態を維持しながら割り込みをインターセプトし、割り込み可能関数に送信できる状態マシンに変換されます。 これはすべて、 Continuation



おかげで可能です。Continuationは、コンパイラーによって、追加の暗黙パラメーターの形式で、中断された関数の各呼び出しに追加されます。 これは、いわゆる継続渡しスタイルです。 より詳細な説明はここにあります







ローマン・エリザロフからのヒント



少し前、私はJetBrainsの Roman Elizarovと話をすることができました。Kotlinのコルーチンがさまざまな方法で登場したおかげです。 この情報をあなたと共有させてください:







Q:最初に出てくる質問は、いつコルーチンを使用する必要があり、スレッドを使用する必要がある状況はありますか?







A:コルーチンは、ほとんどの場合に何かを期待する非同期タスクに必要です。 集中的なCPUタスクのスレッド。







Q:「軽量スレッド」というフレーズは、特にコルーチンがスレッドベースでありスレッドプールで実行されることを考えると、少し誤解を招くように聞こえると述べました。 コルーチンは、実行、中断、停止される「タスク」に似ているように思えます。







A:「軽量ストリーム」というフレーズはかなり表面的なもので、コルーチンは多くの点でユーザーの観点から見るとストリームのように動作します







Q:同期について知りたいのですが。 コルーチンが多くの点でフローに似ている場合、異なるコルーチン間で一般的な状態の同期を実現する必要があります。







A:同期にはよく知られているパターンを使用できますが、コルチンを使用する場合は、一般的な状態をまったく持たないことをお勧めします。 代わりに、コルーチンは「コミュニケーションを通じて交換スタイルを奨励します」。







結論



コルーチンは、 Kotlinに登場した非常に強力な機能です。 コルーチンに出会うまで、 Javaからのマルチスレッド化で十分であるように思えました。







Javaとは対照的に、 Kotlinはまったく異なるスタイルの競合プログラミングを提供します。これは本質的にノンブロッキングであり、膨大な数のネイティブスレッドを強制的に起動することはありません。 Javaでは、これが大きなオーバーヘッドであると考えずに別の追加のスレッドまたは新しいプールを作成することは非常に普通であり、これにより将来アプリケーションが遅くなる可能性があります。 代替としてのコルーチンは、いわゆる「軽量スレッド」であり、それにより、ネイティブスレッドと1対1で相関せず、 deadlocks



starvation



などの問題を起こしにくいことを強調しています。 これまで見てきたように、コルーチンを使用すると、フローのブロックや同期について心配する必要はありません。特に「通信による通信」を順守する場合は、フローはより簡単に見えます。







また、コルーチンを使用すると、競合するコードを作成するためのさまざまなアプローチを使用できます。各アプローチは、ライブラリ( kotlinx.coroutine )に既に実装されているか、その助けを借りて簡単に実装できます。







Java開発者は、タスクをスレッドプールに送信し、 ExecutorService



を使用してfuture



結果を待つことに慣れている可能性が高いですExecutorService



は、 ExecutorService



async/await



を使用して簡単に実装できます。 はい、これは完全に同等の代替品ではありませんが、それでも大きな改善です。







Javaでの並行プログラミングへのアプローチを再定義します。これらすべてのチェック例外、ハードブロッキング戦略、大量の定型コード。 コルーチンの場合、関数をsuspend



呼び出しを使用してコードを連続して記述し、他のコルーチンと通信し、結果を待機し、コルーチンをキャンセルするなど、非常に正常です。







見込み



それにもかかわらず、コルーチンは本当に素晴らしいと確信しています。 もちろん、負荷が高いマルチスレッドアプリケーションにとって本当に成熟しているかどうかは時間が経てばわかります。 多分多くのプログラマーでさえ、プログラミングへのアプローチを考え、再考するでしょう。 次に何が起こるかを見るのは興味深いです。 , , , JetBrains , , .







いいね! . , - . .







Simon








All Articles