Kotlin Coroutine (Experimental 版) のドキュメントを読み解く


Kotlin 1.3 で公式にリリースされる coroutine(コルーチン) 、 今まで丁寧にドキュメントを読んだことがなかったので、 現時点での (Experimental としての) ドキュメントを読んでまとめてみました。

概要

非同期処理を実現する機能です。

ブロックとサスペンド

Coroutine はスレッドをブロックすることなく停止させられる計算のことです。 スレッドのブロックはしばしばコストが高く、計算処理の遅延を引き起こします。 一方で Coroutine の停止はほとんどコストがかかりません。 また、サスペンドファンクションの位置のみでその停止(サスペンド)が起きます。

Suspending Function

サスペンドファンクションです。 次のように記述します。

Suspending Function を呼び出すところで、 停止するか否かは Kotlin のプログラムが自動で決定します。 ですので止まらないこともあります。 Suspending Function は Coroutine または 他の Suspending Function 内で呼び出すことができます。

実際のところ、 Coroutine を実行するには 1つ以上の Suspending Function が必要になります。 例えば async 関数 は Suspending Function を引数に取ります。

次のように async の引数 block に ラムダ を渡すと、 ラムダは Suspending Function として扱われます。

Suspending Function は インターフェース にすることもできます。

@RestrictsSuspension アノテーション

拡張関数とラムダ関数には suspend 修飾子を付与することができます。 これにより、 DSL 作成 や他のユーザが拡張可能なAPIを作ることが可能となります。 いくつかのケースでは、ライブラリの作者はユーザがサスペンドファンクションを追加できないようにする必要があります。

@RestrictsSuspension アノテーション で これを実現することができます。 レシーバクラスまたはインターフェース R@RestrictsSuspension がついている場合、 全ての サスペンディングな拡張関数は R のメンバまたは他の拡張関数に委譲する必要があります。 拡張関数は無限に委譲することができないので(プログラムが終了しません)、ライブラリの作者が完全に制御できる R のメンバを呼び出すことによってすべての中断が確実に行われます。

まれに、すべてのサスペンディングファンクションがライブラリ内で特別な方法で処理される場合に関係します。 たとえば、以下で説明する buildSequence() 関数 を使用してジェネレータを実装する場合、 coroutine のサスペンドの呼び出しが yield() または yieldAll() の呼び出しを終了し、他の関数を呼び出さないようにする必要があります。 これが、SequenceBuilder@RestrictsSuspension アノテーションが付けられている理由です。

Coroutine の内部動作

Coroutine は完全にコンパイル技術によって実装され (VM や OS のサポートは不要です) 、 サスペンションはコード変換によって機能します。 基本的に全てのサスペンディングファンクションは状態がサスペンド呼び出しに該当するステートマシンに変換されます。 停止の前に、次のステートがコンパイラの生成したクラスのフィールドに関連するローカル変数などと共に格納されます。 Coroutine の再開においては、ローカル変数は復元され、ステートマシンは停止後の状態に変化します。

停止された Coroutine は 停止された状態およびローカル変数を保持するオブジェクトとして保存されます。 そのようなオブジェクトの型は Continuation です。 そしてここに記載する全体的なコード変換は古典的な Continuation-passing スタイルです。 そのため サスペンディングファンクション は 内部に Coroutine 型 の 余分なパラメータをとります。

基本のAPI

Coroutine は 3つの要素から成り立っています。

  • サスペンディングファンクションなどの言語サポート
  • Kotlin Standard Library 内の低レベルの中核API
  • ユーザコードから利用できる高レベルのAPI

低レベルAPI: kotlin.coroutines

2つのメインAPIから成り立っています。

  • kotlin.coroutines.experimental
    • createCoroutine()
    • startCoroutine()
    • suspendCoroutine()
  • kotlin.coroutines.experimental.intrinsics: suspendCoroutineOrReturn などの固有の低レベル機能を含む

kotlin.coroutines のジェネレータ

kotlin.coroutines.experimental のアプリケーションレベルの関数は次の通りです。

  • buildSequence()
  • buildIterator()

Sequence に関連しているため これらは kotlin-stdlib 内 にあります。 内部的に Coroutine を使って動作しています。

buildSequence() は低コストで lazy なシーケンスを生成します。 buildIterator() は lazy なイテレータを作成します。

buildSequence の例

この例は1から5までのシーケンスを buildSequence を使って作成するようになっていますが、 呼び出しのところで take(3) を実行しているため 3つの要素しか作成されず、 print("END") は実行されません。 もし lazySeq.take(6) と書いてあれば print("END") まで実行されます。

高レベルAPI: kotlinx.coroutines

Coroutines に関連している API は Kotlin Standard Library から利用可能です。 主に、 Coroutine をベースにしたライブラリが使用するプリミティブおよびインターフェースから成り立っています。

Coroutines をベースとしたほとんどのアプリケーションレベルの API は 分離されたライブラリ kotlinx.coroutines としてリリースされています。 このライブラリは次の範囲をカバーしています。

  • kotlinx-coroutines-core によるプラットフォームに依存しない非同期プログラミング
  • JDK 8 のCompletableFutureに基づくAPI: kotlinx-coroutines-jdk8
  • JDK 7以降のAPIに基づくノンブロッキングIO(NIO): kotlinx-coroutines-nio
  • Swing (kotlinx-coroutines-swing) と JavaFx (kotlinx-coroutines-javafx) のサポート
  • RxJavaのサポート:kotlinx-coroutines-rx

これらのライブラリは、一般的なタスクを簡単に行う便利な API として、 また、コルーチンベースのライブラリを構築する方法に関するエンドツーエンドの例として提供されています。