Kotlin コルーチンの「中断」とは何なのか


Kotlin コルーチンと「中断」

Kotlin のコルーチンについて学んでいると、必ず「中断 (suspension)」という単語に出会います。

学び始めると、だいたいこのような説明を見ることになります。


コルーチンはスレッドのブロッキングではなく、中断を行う。


自分はこの説明を見ても何も理解できませんでした。中断はブロッキングではないのか?

コルーチンを解説しているブログや記事などを読んでも、コルーチンで何ができるのかにフォーカスされていることが多く、コルーチンを理解する上で必要なメンタルモデルを構築できずに、いつもよくわからないままなんとなくわかった気になっていました。

そんな時に出会ったのが八木さんの「Kotlin コルーチンを 理解しよう - Speaker Deck」という資料です。 Kotlin Fest 2018 で発表されたものです。

このスライドではコルーチンがJVMでどのように実現されているのかといった技術的な詳細について述べられており、このスライドをきっかけに中断とはなんなのかがようやく見えた気がしたので、それをまとめたいと思います。


コルーチンはコード変換で実現されている

コルーチンで記述したコードは、コンパイル時にステートマシンに変換されます。中断すると思われる箇所の前後でことなる状態として考え、状態1の処理を実行して中断したあとに、状態2の処理から再開するようなステートマシンです。

正確さを多少犠牲にして簡単にいえば、中断する前とあとで処理を分割(ここでは中断前のものを処理1、後のものを処理2とします)し、処理1を実行して中断箇所で中断したのち、中断が終わると処理2から処理を再開するようなコードに変換されるのです。

例を見てみましょう。

fun main() = runBlocking {

launch {
println("hello")
delay(1000)
println("world")
}
}

このような処理があるとします。プログラムが終了してしまわないように runBlocking で囲っていますが、いまは launch の中だけに注目してください。

このコルーチン (launch のコードブロック) は delay という中断関数を中断箇所として、その前後の2つの処理に分解できます。

さきほどの話でいうところの処理1が

println("hello")

delay(1000)

で、処理2が

println("world")

となります。

コルーチンが実行されると、まず処理1が実行され、 hello と出力されます。そして自らの状態を次の状態に遷移させ、次にコルーチンが実行(再開)されたときには次の処理(処理2)を実行できるようにします。

その後 delay 関数が実行され、コルーチン自体の処理は一旦そこで終わります。これが中断です。

delay 関数は、「何らかの方法で」指定した時間後に自分を実行したコルーチンをもう一度実行(再開)します。

コルーチンは再開されると処理2を実行し、 world が出力されます。

実際のコードとは異なりますが、処理の流れのイメージとしては次のような感じだと思います。(八木さんの資料を参考に作成)

class Coroutine {

private var state = 0
fun run() {
when (state) {
0 -> {
println("hello")
state++
delay(1000, this)
}
1 -> {
println("world")
state++
}
}
}
}

fun delay(time: Long, coroutine: Coroutine) {
Thread {
Thread.sleep(time)
coroutine.run()
}.start()
}

Coroutine の1度目の実行で処理1を実行し、 delay で一定時間後に Coroutine#run() を実行することによって2度目の実行がおこなわれ、その際に処理2が実行されるというイメージです。

この擬似コードでは一定時間後に処理を行うために Thread.sleep() を使用してしまっているためスレッドをブロックしてしまっていますが、実際にはここはもっと複雑な方法(イベントループでスケジューリングとか)が採用されており、スレッドはブロックされないでしょう。


結局「中断」とはなんなのか

処理1を実行した時点で一度処理がコルーチンから抜け、処理1の処理が完了したらまた処理がコルーチンに戻ってきて処理2が実行される。このことを「中断」と表しているのではないかと思いました。


Disclaimer

とは言ったものの、本当はもっと複雑で、 CoroutineContext や Continuation などについての理解をもっと深めればさらに深く理解できるようになるのだと思います。これからも勉強を続けたいと思います。

もしも誤った認識等ありましたら、ご指摘頂けますと幸いです。