この記事は何か?

Kotlin のコルーチンライブラリに kotlinx.coroutines がある。このライブラリには丁寧なドキュメント が付随しており、それを一歩ずつ写経しながら訳していく。なお、全文を訳してはいないし、自分で書き足した部分もある。興味を持っていただけたのであれば、原文にあたることをお勧めする。

はじめの一歩を踏み出す

import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch { // 新しいコルーチンを作りバックグラウンドで動作させる
        delay(1000L) // 1秒待つ (non-blocking)
        println("World!")
    }

    println("Hello, ") // コルーチンが待っている間メインスレッドが動き続ける
    Thread.sleep(2000L) // メインスレッドを止めないように sleep する
}

これの出力結果:

Hello,
World!

コルーチンは軽量なスレッドであり、CoroutineScopelaunch で作成できる。このサンプルでは GlobalScope.launch {} でコルーチンを作っており、これはアプリケーションそのものと同じライフサイクルを持つ。

なお、これと同じことは普通の thread でも実現できる。

import kotlin.concurrent.thread

fun main() {
    thread {
        Thread.sleep(1000L)
        println("World!")
    }

    println("Hello, ")
    Thread.sleep(2000L)
}

ブロックする箇所を明示する

non-blocking な delay() と blocking な Thread.sleep() が混ざると読みにくい。 runBlocking {} を導入して delay() だけで同じことを実現できる。メインスレッドは runBlocking をトリガーし、 runBlocking 内のコルーチンが終了するまでブロックする。

import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch { // 新しいコルーチンを作りバックグラウンドで動作させる
        delay(1000L)
        println("World!")
    }

    println("Hello, ")
    runBlocking { // メインスレッドをブロックするエクスプレッションを作成する
        delay(2000L)
    }
}

さらに runBlocking で main 関数そのものをラップすることもできる。こちらの方がイディオム的である。

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> { // メインコルーチンを開始する
    GlobalScope.launch {
        delay(1000L)
        println("World!")
    }

    println("Hello, ")
    delay(2000L)
}

runBlocking<Unit> としているが、ここの <Unit> は省略可能である。

メインスレッドでコルーチンの終了を待つ

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> { // メインコルーチンを開始する
    val job = GlobalScope.launch { // 子コルーチンを作りジョブのリファレンスを持つ
        delay(1000L)
        println("World!")
    }

    println("Hello, ")
    job.join()  // 子コルーチンの完了を待つ
}

子コルーチンの参照を保持して、メインコルーチンでその join() をコールすることで子コルーチンの完了を待つ。出力結果は前のサンプルと同様ではあるものの、明示的に待ち時間を指定する必要がないので、この方が簡潔である。

構造化された並行性 (Structured Concurrency)

コルーチンが「軽量」であるとはいえ、いくらかのメモリはコルーチンに割り当てられることになる。GlobalScope.launch はトップレベルのコルーチンを作る。これへの参照を保持することを忘れても、これは動かし続けられる。コルーチンがハングしたらどうなるだろう? コルーチンをたくさん立ち上げてメモリを食い尽くしたらどうなるだろう? 起動するすべてのコルーチンを手で管理して join() を呼び出すのはバグのもとになりやすい。

もっとうまいやり方がある。構造化された並行性 (Structured Concurrency) を使うのだ。コルーチンを GlobalScope に生やすのではなく、コルーチンを任意のスコープに生やせるのだ。

この例では main 関数を使っており、それは runBlocking によりコルーチンになっている (runBlocking はコルーチンビルダーである)。runBlocking を含む全てのコルーチンビルダーは CoroutineScope のインスタンスを作り、それを自身のコードブロックのスコープに追加する。このスコープ内で明示的に join() を呼ぶ必要のない新しいコルーチンを作ることができる。なぜなら、外側のコルーチンは内部のコルーチンすべてが終了するまで完了できないからだ。したがって、コードをより簡潔に書くと次のようになる。

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch { // runBlocking スコープで新しいコルーチンを起動する
        delay(1000L)
        println("World!")
    }

    println("Hello, ")
}

スコープビルダー

coroutineScope ビルダーで独自のスコープを作ることができる。coroutineScope で作成したコルーチンが終了するまで、プログラムは終了しない。 coroutineScoperunBlocking と違い、すべての子コルーチンを待つものの、現在のスレッドをブロックしない。

サンプルコードを眺めてみよう。

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        delay(200L)
        println("Task from runBlocking")
    }

    coroutineScope {
        launch {
            delay(500L)
            println("Task from nested launch")
        }

        delay(100L)
        println("Task from coroutine scope")
    }

    println("Coroutine scope is over")
}

このコードの実行結果は次のようになる。

Task from coroutine scope
Task from runBlocking
Task from nested launch
Coroutine scope is over

Extract function refactoring (“関数として切り出す” リファクタリング)

launch {} の中を関数として切り出してみよう。この場合 suspend 修飾子を関数に付与する必要がある。通常の関数と同じく suspend 付きの関数はコルーチンの中で使えるし、さらに他の suspend 関数 (たとえば delay) もコールできる。

suspend 付き関数を使い、次のようにリファクタリングできる。

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch { doWorld() }
    println("Hello, ")
}

suspend fun doWorld() {
    delay(1000L)
    println("World!")
}

切り出した関数が、そのスコープから起動するコルーチンビルダーを含んでいたらどうなるだろう? この場合は切り出した関数に suspend をつけるだけでは不十分である。 doWorld 関数を CoroutineScope に載せるという案もあるが、設計が汚くなる。

コルーチンは軽量である

次のコードを実行してみよう。

import kotlinx.coroutines.*

fun main() = runBlocking {
    repeat(100_000) { // たくさんのコルーチンを起動する
        launch {
            delay(1000L)
            print(".")
        }
    }
}

100k のコルーチンを作成し、1秒後に各々のコルーチンがドットを出力する。もしこれをスレッドで行ったらどうなるだろう? (きっと OutOfMemory 系のエラーに遭遇することでしょう)

グローバルコルーチンはデーモンスレッドのように振る舞う

The following code launches a long-running coroutine in GlobalScope that prints “I’m sleeping” twice a second and then returns from the main function after some delay:

次のコードは GlobalScope の中で長時間動くコルーチンを作成し、それが “I’m sleeping” と1秒に二回ほど出力したあとでメイン関数から抜け出す。

import kotlinx.coroutines.*

fun main() = runBlocking {
    GlobalScope.launch {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
    }
    delay(1300L) // 少し待って終了する
}

実行すると次の3行が出力される。

I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...

GlobalScope 内で起動したアクティブなコルーチンは処理を継続できない。これらはデーモンスレッドのように振る舞う。