Kotlin コルーチン(Coroutines)

Coroutine

Kotlinはバージョン1.1からCoroutineを導入した。

Kotlinは言語として、さまざまなライブラリのcoroutineを使用できるように、標準ライブラリでは最小限のlow-level APIだけを提供している。Kotlinではasyncとawaitはキーワードに含まれず、標準ライブラリの一部でもない。また、Kotlinのsuspend機能の概念は、futuresやpromiseより安全で、エラーが発生しにくい非同期処理の抽象化を提供する。

Coroutineは軽量Thread

Coroutineは軽量スレッド(Light-weight thread)と言える。スレッドではないが、非同期(asynchronous)プログラミングを可能にする。
Threadと同じように、他の処理をブロックせずに並列で処理できる。しかし非常に軽量である。
Threadは開始と終了に無視できないコストがかかるが、Coroutineはほとんど気にする必要がない程度のコストしかかからない。
同時に数千、数万個のCoroutineを簡単に実行できる。

CoroutinesはCo + Routinesの略で、CoはCooperationを意味し、Routinesはfunctionsを意味する。互いに協力する関数という意味で、これを簡単にCoroutineと呼ぶ。

Gradle dependencyの設定

コルーチンを使用するには、次の依存関係を設定する必要がある。

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.2")
}

追加の依存関係はhttps://github.com/Kotlin/kotlinx.coroutines で設定できる。

Coroutinesの基礎

coroutineを理解するには、suspend(中断)の概念を知る必要がある。coroutineではthreadをblock(占有)する代わりに、処理をsuspend(中断)する。blockとsuspendの違いを見てみよう。

block
blockはスレッドを占有する。占有すると、そのthreadで処理を進められなくなる。

fun main(){
    println("Coroutine")
    Thread.sleep(3000L) // 3秒間処理が停止する。この間、処理はまったく進まない。
    println("Hello")
}

Output:

Coroutine
Hello

suspend
suspendはcoroutine処理を中断し、threadを解放する。解放している間、他の処理にリソースを活用できる。

import kotlinx.coroutines.*

fun main(): Unit = runBlocking {
    launch {
        delay(3000L) // 3秒間処理を中断する。スレッドが解放されるため、先にHelloが表示される。
        println("Coroutine")
    }
    println("Hello")
}

Output:

Hello
Coroutine

blockの場合、3秒間threadが占有されてしまう問題がある。たとえば、applicationの使用中にthreadを占有してしまうとfreezeが発生し、userが画面を操作できなくなる。
一方、suspendの場合は中断するだけなので、3秒間userは操作できる。

coroutineを使用するには、suspend(中断)を正しく理解する必要がある。

最初のコルーチン

coroutineはsuspendable computationのinstanceである。他のcodeと同時に動作するcode blockを実行するという意味で、threadと概念的に似ている。しかしcoroutineは特定のスレッドにバインドされない。あるスレッドで実行をsuspendし、別のスレッドで再開できる。

coroutineは軽量threadと考えることができるが、実際の使い方はthreadとは大きく異なる点がいくつかある。

fun main() = runBlocking {  // this: coroutineScope
    launch { // 新しいcoroutineを開始
        delay(1000L) // non-blockingで1秒遅延
        println("World!") // 1秒後に表示
    }
    println("Hello") // 前のスレッドが遅延している間、基本スレッドが実行される。
}

Output:

Hello
World!

続いて、このコードが何をしているかを分析してみよう。

  • launchはcoroutine builderである。残りのコードと同時に新しいcoroutineを開始し、独立して動作し続ける。そのためHelloが最初に表示された。
  • delayは特殊なsuspend関数である。coroutineを特定の時間だけ中断し、基本スレッドを解放する。delayを持つcoroutineは中断されるが、基本スレッドはブロックされない(non-blocking)。
  • runBlockingは、fun main()のcoroutineではない世界と、runBlocking {...}のblock内のcoroutineをつなぐcoroutineでもある。たとえば、launchはcoroutineScope内でのみ宣言される。

runBlockingという名前は、runBlocking {...}内のすべてのcoroutineが実行を完了するまで、それを実行するthread(この場合はmain thread)がcall中にblockされることを意味する。

高価なresourceであるthreadをblockするのは非効率で望ましくないため、実際のcode内でrunBlockingが使用されることはほとんどない。

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

coroutineはStructured concurrencyの原則に従う。つまり、新しいcoroutineは、coroutineのライフサイクルを区分する特定のcoroutineScope内でのみ開始できる。実際のアプリケーションでは多くのcoroutineが開始されるが、Structured concurrencyを使用すると失われたり漏れたりしない。

抽出関数リファクタリング(Extract function refactoring)

launch {...}内のcode blockを別の関数に抽出してみよう。coroutineScopeからコードを抽出する場合は、suspend修飾子を使用する。

import kotlinx.coroutines.*

fun main() = runBlocking {  // this: coroutineScope
    launch { doWorld() }
    println("Hello")
}

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

Output:

Hello
World!

スコープビルダー(Scope builder)

さまざまなビルダーが提供するcoroutine scopeのほかに、coroutineScopeビルダーを使用して自分のscopeを宣言できる。coroutine scopeを作成すると、起動されたすべての処理が完了するまで完了しない。

runBlockingビルダーとcoroutineScopeビルダーは、どちらも自身の処理とすべての子スレッド処理が完了するのを待つため、似ているように見える。主な違いは次のとおりである。

  • runBlockingは、子スレッドが完了するまで現在のスレッドをblockする。
  • coroutineScopeは、子スレッドが完了するまで現在のスレッドをblockしない。

この違いにより、runBlockingは通常の関数であり、coroutineScopeはsuspend関数として実行される。

fun main() = runBlocking {
    doWorld()
}

suspend fun doWorld() = coroutineScope {  // this: coroutineScope
    launch {
        delay(1000L)
        println("World!")
    }
    println("Hello")
}

Output:

Hello
World!
fun main() = runBlocking { // serial
    countToDown()
    println("fire!")
}

suspend fun countToDown() = coroutineScope { // concurrent
    launch {
        delay(2000L)
        println("1")
    }
    launch {
        delay(1000L)
        println("2")
    }
    println("3")
}

Output:

3
2
1
fire!

明示的なJob(An explicit job)

launch coroutineビルダーは、開始されたcoroutineへのハンドルであり、完了を明示的に待機するために使用できるJobオブジェクトを返す。たとえば、次の例では子coroutineが完了するまで待機した後、文字列"Done"が出力される。

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch { // 新しいcoroutineを開始し、jobへの参照を保持する。
        delay(1000L)
        println("World!")
    }
    println("Hello")
    job.join() // 子coroutineが完了するまで待つ。
    println("Done")
}

Output:

Hello
World!
Done

コルーチンは軽量である(Coroutines are light-weight)

100K個のcoroutineを開始し、5秒後に各coroutineが点(.)を出力する。

軽量なcoroutineで実行すると成功するが、Threadで実行するとjava.lang.OutOfMemoryErrorが発生する。

import kotlinx.coroutines.*

fun main() {
    launchCoroutines() // success!
    runThreads() // java.lang.OutOfMemoryError
}

private fun launchCoroutines() = runBlocking {
    repeat(100_000) { // launch a lot of coroutines
        launch {
            delay(5000L)
            print(".")
        }
    }
}

private fun runThreads() {
    repeat(100_000) { // launch a lot of threads
        object : Thread() {
            override fun run() {
                sleep(5000L)
                print(".")
            }
        }.start()
    }
}

まとめ

今回学んだ内容のうち、重要だと思う点を整理した。

  • コルーチンは軽量で安全に処理できる非同期処理の1つである。
  • threadはblockせず、suspend(中断)を使用する。
  • 特定のスレッドにバインドされないため、別のスレッドで再開できる。
  • coroutineScope内で開始されたcoroutineは、すべての下位処理が完了するまで完了しない。

参考