Kotlin では バージョン 1.1 から Coroutine が導入されています。
検索すると coroutine, async/await に関する情報がたくさん見つかりますが、そもそも coroutine がどのようなもので、どのように使うのかを簡単に説明した資料が少なかったため、 Kotlin 公式ドキュメントを元に勉強してみました。
筆者は Coroutine 自体に馴染みがなく、今回 Kotlin で初めて触れています。根本的な認識の誤り等なにかございましたらご指摘いただければ嬉しく思います。
※ 2017年11月現在の最新版は 1.1.60 ですが、 Coroutine は未だ experimental となっています。そのため今後破壊的な変更が行われる可能性があります。
Coroutine は軽量な Thread
Coroutine とは、つまるところ軽量な Thread のようなものです。
Thread のように他の処理をブロックすることなく並列に処理を行えます。
しかしとても軽量です。
Thread はその開始と終了に無視できないコストがかかるのに対して、 Coroutine はほとんど気にする必要のない程度のコストしかかかりません。
同時に数千数万の Coroutine を容易に実行することができます。
launch
Coroutine を作成して利用するのはとても簡単です。
launch {
// 何らかの処理
}
これだけでラムダ内の何らかの処理をその実行スレッドをブロックすることなく並列に処理することができます。
次のプログラムを実行するとどうなるでしょうか?
fun main(args: Array<String>) {
println("start")
launch {
println("coroutine!")
}
println("end")
}
実はこれでは次のように出力されてしまいます。
start
end
launch 内の処理が実行されていません。
なぜなら、 launch はその実行スレッドを全くブロックしないからです。
"start"
が出力され、 launch
が実行された直後にすぐに "end"
が出力されてプログラムは終了します。
launch のラムダが実行されるよりも先にプログラムが終了しているのです。
fun main(args: Array<String>) {
println("start")
launch {
println("coroutine!")
}
Thread.sleep(1000)
println("end")
}
このようにすると、期待通り次のように出力されます。
start
coroutine!
end
launch のラムダ内の処理が完了するまでの十分な時間だけスレッドを停止させてから、 "end"
と出力して終了するためです。
runBlocking
runBlocking
はその名の通り、現在のスレッドをブロックします。
先程のプログラムを以下のように変更します。
fun main(args: Array<String>) {
println("start")
runBlocking {
println("coroutine!")
}
println("end")
}
実行すると以下のように出力されます。
start
coroutine!
end
Corotuine が終了するまで待ってから、 "end"
が出力されることがわかります。
また、 runBlocking
は実行した Coroutine の戻り値を取得することもできます。
次のように変更します。
fun main(args: Array<String>) {
println("start")
val text = runBlocking {
"coroutine!"
}
println(text)
println("end")
}
この場合も先程と同じく、"start"
, "coroutine!"
, "end"
の順に出力されます。
ちなみに runBlocking
は Coroutine 内から呼び出すべきではありません。
従来型のブロッキング処理と Coroutine で実装された処理の橋渡しとしてや、例のように main 関数内での使用、またはテストでの使用などを想定して設計されているためです。
Job#join()
runBlocking
は現在のスレッドをブロックするため、コルーチンとスレッドの境界でしか使用すべきではありません。
コルーチン内で他のコルーチンの終了を待機するために Job#join
が利用できます。
launch
は戻り値に Job
インスタンスを返すため、上の例は次のように変更できます。
fun main(args: Array<String>) = runBlocking() {
println("start")
launch {
println("coroutine!")
}.join()
println("end")
}
async/await
runBlocking
を使用すれば、 Coroutine の戻り値を取得することができました。
しかし、現在のスレッドをブロックして Coroutine の戻り値を得られるというのは非同期処理を行いたい場合はあまり嬉しくありません。
launch {}.join()
のように他のコルーチンを非ブロッキングで中断し、かつ runBlocking
のように終了時に戻り値を受け取りたい場合はどうすればよいでしょうか。
async と Deferred<T>
そこで async
の出番です。
async
は Coroutine を作成し、戻り値として Deferred<T>
型のオブジェクトを返します。
Deferred<T>
型は future のようなもので、 Coroutine の処理が終わったタイミングでその戻り値を取得することができます。
まず、先程からの例を次のように変更します。
fun main(args: Array<String>) {
println("start")
async {
println("coroutine!")
}
println("end")
}
しかしこれでは "coroutine!"
は出力されません。 launch
の時と同じく Coroutine が実行され "coroutine!"
が出力されるよりも先にプログラムが終了しているためです。
await
やはり Coroutine の終了を待つ必要があります。
async
の返す Deferred<T>
インタフェースには await
メソッドが定義されています。
await
メソッドは、 async
が起動したコルーチンの終了まで現在のコルーチンを中断し、終了したコルーチンの戻り値を取得します。
fun main(args: Array<String>) = runBlocking() {
println("start")
val text = async {
"coroutine!"
}.await()
println(text)
println("end")
}
Suspending Function
ところで、 join
メソッドや await
メソッドのような 、Coroutine を中断させることのできる関数を Suspending Function
と言います。
suspend
modifier を付けて関数を宣言することで、その関数が Coroutine を中断可能であることを示すことができます。
suspend fun doSomething() {
delay(1000)
println("something")
}
この例では doSomething
関数は 1 秒待ってから "something"
と出力します。
delay
も Suspending Function として定義されており、その Coroutine の処理を指定した時間だけ中断(停止)させることができます。
suspend
と宣言された関数は Coroutine 内または他の Suspending Function 内からしか呼び出すことができません。
また Coroutine を開始するには最低 1 つの Suspending Function がなければなりません。
先程から launch
や runBlocking
ではラムダを使用して Coroutine を作成していましたが、実はこれらは無名の Suspending Lambda だったのです。
launch のスキーマは次のようになっています。
public fun launch(
context: CoroutineContext = DefaultDispatcher,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
第3引数が匿名 Suspending Lambda になっています。
これらを踏まえた上で、次のようにプログラムを変更します。
fun main(args: Array<String>) {
println("start")
runBlocking {
println("coroutine start")
greet()
println("coroutine end")
}
println("end")
}
suspend fun greet() {
delay(1000)
println("Hello, Coroutine!")
}
"start"
, "coroutine start"
とすぐに表示され、 1 秒間経ってから "Hello, Coroutine!"
, "coroutine end"
, "end"
と順に出力されます。
定義した greet()
関数が runBlocking で作成した Coroutine の処理を中断し、再開しているのがわかります。
まとめ
- Coroutine は軽量な Thread と考えることができる
- Coroutine を作成するコストは Thread とは比較できないほど軽微である
-
launch
,runBlocking
,async
などを用いて Coroutine を開始できる -
runBlocking
では同期的に、async
では非同期的に Coroutine の戻り値を取得できる - Suspending Function とは Coroutine を中断可能な関数のことである
- Coroutine を実行するには Suspending Function が必要である
-
launch
,runBlocking
,async
などは引数に匿名 Suspending Funtion を取ることで Coroutine を開始している - Suspending Function は Coroutine か Suspending Function からしか実行できない
- Coroutine 楽しい!