Kotlin
coroutine
AsyncAwait

Kotlin の Coroutine について

https://raw.githubusercontent.com/JetBrains/kotlin-web-site/master/pages/docs/reference/coroutines.md の雑な訳。Apache 2.0.


Coroutines - コルーチン


コルーチンは Kotlin 1.1+ において 試験的な 機能。


いくつかの API は長期間における処理をする(NetworkID, file IO, CPU- or GPU-intensive work 等)その場合呼び出し元をブロックしてしまうのだけど、コルーチンはブロックをさけ、ブロックを 一次停止する手段を提供する。

コルーチンによって、複雑性がライブラリの中に隠蔽されて非同期プログラミングがよりシンプルになる。プログラムのロジックはコルーチンの中で sequentially として表現されて、基盤を提供するライブラリが非同期性を処理してくれる。ライブラリはユーザーコードをコールバックにラップ、イベントの購読、他のスレッドでの実行などを管理する。それでもコードは順次実行のようなシンプルさを保つ。

他の言語で利用されている非同期メカニズムもKotlinコルーチンを利用して実装する事ができる。以下を含む

See the description below for libraries providing such constructs.


ブロッキング v.s. サスペンディング

基本的に、コルーチンの処理はスレッドブロッキング無しにサスペンド可能。スレッドブロッキングは得てしてコストが高い(特にロードが高い場合は)。何故なら比較的少ない数のスレッドだけを保持できるので、一つのスレッドがブロックすると他の作業を送らせることになってしまうから。

コルーチンのサスペンディングはほとんどコストが無い。コンテキストスイッチや他の OS の関与が不要。さらになにより、これはユーザーライブラリによってコントロールされるので非常に高い拡張の余地がある。ライブラリの作者によってサスペンションのときに最適化/ログ/インターセプトなどを必要に応じて実装できる。

コルーチンのもうひとつのちがいとして、コルーチンは任意の場所で停止されることは無く、 suspension points と呼ばれる特別にマークされた関数のみで停止される。


サスペンディング関数

サスペンドは suspendとマークされた特別な関数でのみ起こる:

suspend fun doSomething(foo: Foo): Bar { ... }

このような関数は suspending functions と呼ばれる。何故ならこれを呼ぶタイミングでcoroutine がサスペンドするかもしれないから (ライブラリ側で、実行準備が整っていないと判断すればサスペンドしないこともある). サスペンディングファンクションはパラメーターや返値を持つことは普通の関数と同じだが、コルーチンや他のサスペンディングファンクションからしか呼べないという違いがある。インライン展開された関数でも同じ。

実際に、コルーチンを開始するためにはサスペンディング関数が必要で、よくサスペンディングラムダで表現される。 例えば例はこのような物: async() function (from the kotlinx.coroutines library):

fun <T> async(block: suspend () -> T)

この async() は通常関数で (suspending functionではない), だが、 block パラメーターを取るラムダはサスペンディングファンクションで、 suspending lambda と呼ばれる。この中からはサスペンディングファンクションを呼ぶことが可能で

async {

doSomething(foo)
...
}


ノート: 現在、サスペンディングファンクションの型は supertype としては使えず、非同期サスペンディング関数もサポートされない。


コルーチンのアナロジーとして await() もまたサスペンディングファンクションであり (そのため、 async {} blockから呼び出せる)他のコルーチンの実行まで、コルーチンをサスペンドさせる

async {

...
val result = computation.await()
...
}

async/await 関数がどのように動くかの詳細は kotlinx.coroutines の中で見つけることができる こちら.

await()doSomething() は通常のファンクション、インライン化されていないファンクションリテラルや通常 main の中では呼び出せない。

fun main(args: Array<String>) {

doSomething() // ERROR: Suspending function called from a non-coroutine context

async {
...
computations.forEach { // `forEach` is an inline function, the lambda is inlined
it.await() // OK
}

thread { // `thread` is not an inline function, so the lambda is not inlined
doSomething() // ERROR
}
}
}

更に、サスペンディング関数は仮想関数たりえ、オーバーライドされる場合も suspend modifier の指定が必須である。

interface Base {

suspend fun foo()
}

class Derived: Base {
override suspend fun foo() { ... }
}


@RestrictsSuspension アノテーション

Extension functions (and lambdas) も suspend とマークが可能で通常の suspend 関数と同様に動く. これによって DSLs や他の API もこの拡張を使うことができる。。いくつかのケースでは、ライブラリの作者は、Suspending function の中でユーザーが任意にサスペンドすることを禁止したいかもしれない。

そのため @RestrictsSuspension アノテーションを使うことができる。レシーバークラスやインターフェース R がこれでアノテーションされると、全てのサスペンド拡張は R メンバーへ委譲または。エクステンション同士はお互い無制限に委譲することはできないので、これはライブラリ作者が R のメンバーを呼んでいるタイミングのサスペンドをライブラリ作者が完全にコントロールできることを意味する。(ここらへんなんとなく解るけど訳はゴミ

想定的に希なケースとして、全てのサスペンドはライブラリで把握したいと思うかも知れない。例えば、buildSequence() 関数を(see below) を実装しているときなどに、以下の事を確実にする必要がああります。


コルーチン内のどのようなsuspending call も yeld() または yeldAll() で終了し、他の関数では終了しない。


これが SequenceBuilder@RestrictsSuspension でアノテーションされている理由です:

@RestrictsSuspension

public abstract class SequenceBuilder<in T> { ... }

See the sources on Github.


コルーチンの内部挙動

このばで詳細な開設はしないが、何が重要かだけ。

コルーチンはコンパーラー技術で実装されていて(JVM または OS 技術は利用していない)、サスペンションはコード変換で動く。基本的に、全てのサスペンディング関数(最適化が実行されるかも知れないがここでは触れない)は、サスペンディングコールに対応したステートマシーンに変換される。サスペンションがおこると、次の状態がコンパイラが生成したフィールドに保存される(実行場所と、ローカル変数など)。コルーチンがレジュームされると、ローカル変数がリストアされて、中断された場所から実行が再開される。

サスペンドされたコルーチンは保存、引き渡し可能。そのようなオブジェクトの型は Continuation。ここで触れたような全体的なコード変換の詳細は次で詳細に書かれている corresponds to the classical Continuation-passing style. 従って、サスペンド関数は continuation という追加引数もを持っている。

コルーチンがさらにどのように動くかの詳細は、 this design document で見つかる。実装された言語機能は Kotlin のコルーチンほど一般的ではないかもしれないが、他の言語での async / await (C#やECMAScript 2016など)の同様の記述も参考になる。


コルーチンの状況

コルーチンのデザインは 実験的 であり, 将来のリリースで変更される可能性がある. コンパイル時にフラグを設定しないと The feature "coroutines" is experimental が表示される。非表示にするためには明示的なopt-in flag が必要.

エクスペリメンタルなステータスのため、コルーチン関係の API で標準ライブラリに同梱されている物は kotlin.coroutines.experimental package にある. デザインが Fix されたら kotlin.coroutinesにリフトアップされる予定。experimental package 自体は後方互換性のため(他のアーティファクトとして?)維持されるんじゃないかな.

重要: ライブラリ作者にも同じコンベンションに従うことをオススメ "experimental" を追加して、例えば com.example.experimental のような suffix をコルーチンを使うパッケージに使って欲しい。最終的に APIが Fix したら

* 全ての API を com.example にコピーし(experimental 接頭辞を削除する),

* experimental package を後方互換性のために残しておく

これによって、ユーザーのマイグレーション問題を最小化できる


Standard APIs

コルーチンの中核は以下の3つ

Coroutines come in three main ingredients:

- 言語サポート (i.s. suspending functions, 上で書いた物);

- Kotlin Standard Library の 低レベルコアAPI

- 直接ユーザーコードに現れる high-level APIs


Low-level API: kotlin.coroutines

ローレベルAPIは比較的小さい。高レベルAPIを作ること以外に使われるべきでない。2つのパッケージからなって居る。

- kotlin.coroutines.experimental 主な型とプリミティブがある。例えば:

-- createCoroutine(),

-- startCoroutine(),

-- suspendCoroutine();

- kotlin.coroutines.experimental.intrinsics 同じく低レベルの命令。例えば suspendCoroutineOrReturn.

これらの API の更に詳しい用法は here.


Generators API in kotlin.coroutines

kotlin.coroutines.experimental のうち、"application-level" の関数は以下の二つ

- buildSequence()

- buildIterator()

これらはシーケンスとも関係があるので、 kotlin-stdlib の一部として公開されている。実際、これらの関数が generator を実装している。

import kotlin.coroutines.experimental.*

fun main(args: Array<String>) {
//sampleStart
val fibonacciSeq = buildSequence {
var a = 0
var b = 1

yield(1)

while (true) {
yield(a + b)

val tmp = a + b
a = b
b = tmp
}
}
//sampleEnd

// Print the first five Fibonacci numbers
println(fibonacciSeq.take(8).toList())
}

この generates は yield() function を呼ぶことで、遅延評価で、潜在的に無限のフィボナッチ数列を生成している。シーケンスをイテレーションするときに、次の番号が生成される。すなわち、我々はどんな有限のリストもこのシーケンスから取り出す事ができる。そして、コルーチンはこれを実用的に実施するのに十分簡単な手段となっている。

このようなシーケンスの真の遅延評価のデモをするために、ビルドシーケンス内で、いくつかデバッグアウトプットを実施する事ができる。

import kotlin.coroutines.experimental.*

fun main(args: Array<String>) {
//sampleStart
val lazySeq = buildSequence {
print("START ")
for (i in 1..5) {
yield(i)
print("STEP ")
}
print("END")
}

// Print the first three elements of the sequence
lazySeq.take(3).forEach { print("$it ") }
//sampleEnd
}

このコードを実行すると最初の3要素が print される。数字は STEP と交互に表示される。これは、処理が遅延で行われたことの例である。

collection または sequence の数値をまとめて評価するために yieldAll() 関数が使える:

import kotlin.coroutines.experimental.*

fun main(args: Array<String>) {
//sampleStart
val lazySeq = buildSequence {
yield(0)
yieldAll(1..10)
}

lazySeq.forEach { print("$it ") }
//sampleEnd
}

buildIterator()buildSequence() と同じような処理をする, ただし返すのは遅延評価のイテレーターである。

SequenceBuilder class (that bears the @RestrictsSuspension annotation described aboveに対するサスペンディングエクステンションを書くことで、buildSequence() に custom yielding logic を追加できる :

import kotlin.coroutines.experimental.*

//sampleStart
suspend fun SequenceBuilder<Int>.yieldIfOdd(x: Int) {
if (x % 2 != 0) yield(x)
}

val lazySeq = buildSequence {
for (i in 1..10) yieldIfOdd(i)
}
//sampleEnd

fun main(args: Array<String>) {
lazySeq.forEach { print("$it ") }
}


他の高レベル API: kotlinx.coroutines

コルーチンに関連して core API だけが Kotlin Standard Library で利用可能。

ほとんどのコルーチンを基本にしたアプリケーションレベル API は他のライブラリとしてリリースされている: kotlinx.coroutines.このライブラリは以下をカバー

* プラットフォーム非依存の非同期プログラミングを可能にする kotlinx-coroutines-core:

* このモジュールは、Go-like な channels を提供。channelselect および他の便利なプリミティブを用意,

* kotlinx-coroutines-coreの包括的ガイド;

* JDK8 の CompletableFuture のための API: kotlinx-coroutines-jdk8;

* JDK7 以上の Non-blocking IO (NIO) のための API: kotlinx-coroutines-nio;

* Support for Swing (kotlinx-coroutines-swing) and JavaFx (kotlinx-coroutines-javafx);

* RxJava サポート: kotlinx-coroutines-rx.

これらのライブラリは、共通処理を簡易に行うための便利なAPI群を提供し、また、コルーチンを基本にしたライブラリを提供するための end-to-end の例にもなっている。