Kotlin のコルーチンとは、スレッドを止めずに「待つ」ことができる、軽量な並行処理の仕組みです。 通信やファイルの読み込みのように待ち時間のある処理を、上から下へ読める素直なコードのまま、同時に進められます。

この記事では、Gradle(Kotlin DSL)の依存の書き方から始めて、runBlocking・launch・async・coroutineScope・supervisorScope・withContext の6つを「戻り値・呼び出し元を待たせるか・例外の伝わり方」で比較します。直列と並列の所要時間の実測、スレッド名で見る Dispatchers の違い、キャンセル・タイムアウト・例外のエラー文まで、すべて実際に動かした出力を載せています。

確認環境:Ubuntu 24.04 / Kotlin 2.4.20(kotlinc)/ kotlinx-coroutines-core 1.8.0 / OpenJDK 21.0.10(CPU 2コア)。掲載した出力はこの環境で実行して得たものです。所要時間とスレッド名の番号は、環境によって変わります。

コルーチンの主な関数の早見表

関数 戻り値 呼び出し元を待たせるか 中で起きた例外の伝わり方 主な用途
runBlocking ブロックの結果 スレッドごと止めて待つ 呼び出し元にそのまま投げられる main 関数やテストの入口
launch Job 待たせない(すぐ次へ進む) 親のコルーチンへ自動で伝わる 結果のいらない処理を起動する
async Deferred(あとで結果を受け取る入れ物) 待たせない(await() で待つ) await() を呼んだ場所で投げられる 結果のいる処理を並列に進める
coroutineScope ブロックの結果 中の子が全部終わるまで中断して待つ 子が1つエラーになると残りの子をキャンセルし、呼び出し元へ投げる 複数の子をひとまとめにする
supervisorScope ブロックの結果 中の子が全部終わるまで中断して待つ 子のエラーがほかの子に広がらない 1つのエラーで全体を止めたくないとき
withContext ブロックの結果 終わるまで中断して待つ 呼び出し元にそのまま投げられる 実行するスレッドを切り替える

「中断して待つ」とは、スレッドを止めずに、そのコルーチンだけが待つという意味です。待っている間、スレッドはほかのコルーチンを進められます。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Kotlinコルーチン入門|launch・async・runBlocking」

準備:依存を追加する

コルーチンは標準ライブラリとは別の kotlinx.coroutines というライブラリで提供されています。Gradle(Kotlin DSL)では、build.gradle.kts に次のように書きます。kotlinx.coroutines 公式リポジトリの README に載っている、2026年10月時点の書き方です。

// build.gradle.kts
plugins {
    kotlin("jvm") version "2.2.20"
}

repositories {
    mavenCentral()
}

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

README では、最新版は 1.11.0 で、Kotlin 2.2.20 に対応した版と案内されています。リポジトリの一覧に mavenCentral() を入れておくことも書かれています。

この記事のコードは、kotlinc とライブラリの jar を直接指定して動かしました。Gradle を使わずに試したいときは、次のコマンドで実行できます。

kotlinc -cp kotlinx-coroutines-core-jvm.jar hello.kt -include-runtime -d hello.jar
java -Dstdout.encoding=UTF-8 -cp hello.jar:kotlinx-coroutines-core-jvm.jar HelloKt

hello.kt をコンパイルすると HelloKt というクラスができるので、それを指定して実行します。-Dstdout.encoding=UTF-8 は日本語の出力を UTF-8 で表示するための指定です。

最初のコルーチン:runBlocking と launch

// hello.kt
import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        delay(1000)
        println("World!")
    }
    println("Hello,")
}

実行結果:

Hello,
World!

コードでは launch が先に書かれていますが、出力は "Hello," が先です。動きを順に追います。

順番 動き
1 runBlocking がコルーチンの世界への入口を作る
2 launch が新しいコルーチンを起動し、すぐ次の行へ進む
3 println("Hello,") が実行される
4 起動したコルーチンは delay(1000) で1秒待ってから "World!" を出力する
5 runBlocking は中のコルーチンが全部終わるのを待ってから終了する

delay は、スレッドを止めずに指定した時間だけ待つ関数です。 Thread.sleep と違い、待っている間にスレッドがほかのコルーチンを進められます。

suspend 関数:中断できる関数を自分で作る

delay のように「中断できる関数」を suspend 関数(サスペンド関数)と呼びます。自分で作るときは、fun の前に suspend を付けます。

suspend fun fetchUser(): String {
    delay(1000)          // 1秒かかる処理のかわり
    return "user"
}

suspend 関数は、コルーチンの中か、別の suspend 関数の中からだけ呼べます。ふつうの関数から呼ぶとコンパイルエラーになります。

import kotlinx.coroutines.*

suspend fun fetchUser(): String {
    delay(100)
    return "user"
}

fun main() {
    val user = fetchUser()
    println(user)
    launch { println("x") }
}

コンパイル結果:

c12_ng.kt:9:16: error: suspend function 'suspend fun fetchUser(): String' can only be called from a coroutine or another suspend function.
    val user = fetchUser()
               ^^^^^^^^^
c12_ng.kt:11:5: error: unresolved reference 'launch'.
    launch { println("x") }
    ^^^^^^
エラー文 理由 直し方
suspend function ... can only be called from a coroutine or another suspend function. ふつうの関数から suspend 関数を呼んだ fun main() = runBlocking { ... } の中で呼ぶ
unresolved reference 'launch'. launch はスコープ(コルーチンの入れ物)の中でだけ使える runBlocking や coroutineScope の中で呼ぶ

直列と並列:async と awaitAll で所要時間を比べる

1秒かかる処理を2つ用意し、順番に呼んだ場合と、async で同時に進めた場合の時間を測りました。

// parallel.kt
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

suspend fun fetchUser(): String {
    delay(1000)          // 1秒かかる処理のかわり
    return "user"
}

suspend fun fetchOrders(): List<String> {
    delay(1000)
    return listOf("order-1", "order-2")
}

fun main() = runBlocking {
    val t1 = measureTimeMillis {
        val user = fetchUser()
        val orders = fetchOrders()
        println("直列: $user / $orders")
    }
    println("直列の所要時間: $t1 ms")

    val t2 = measureTimeMillis {
        val user = async { fetchUser() }
        val orders = async { fetchOrders() }
        println("並列: ${user.await()} / ${orders.await()}")
    }
    println("並列の所要時間: $t2 ms")

    val t3 = measureTimeMillis {
        val results = (1..10).map { n ->
            async {
                delay(1000)
                n * n
            }
        }.awaitAll()
        println("awaitAll: $results")
    }
    println("10件を並列にした所要時間: $t3 ms")
}

実行結果:

直列: user / [order-1, order-2]
直列の所要時間: 2023 ms
並列: user / [order-1, order-2]
並列の所要時間: 1009 ms
awaitAll: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
10件を並列にした所要時間: 1008 ms
書き方 所要時間 動き
suspend 関数を順番に呼ぶ 2023 ms 1つ目が終わってから2つ目を始める
async を2つ起動して await() 1009 ms 2つを同時に進めて、両方の結果を待つ
async を10個起動して awaitAll() 1008 ms 10個を同時に進めて、結果をリストで受け取る

suspend 関数をそのまま並べると直列、async で包むと並列になります。 10件に増やしても所要時間は約1秒のままでした。awaitAll() の結果は、起動した順番どおりに並びます。

async は起動してから await する

async { ... }.await() と続けて書くと、起動してすぐ待つことになり、直列と同じ時間がかかります。先にすべて起動して変数に入れ、あとで await() するのが並列にするコツです。

// 並列になる書き方
val user = async { fetchUser() }
val orders = async { fetchOrders() }
println("${user.await()} / ${orders.await()}")

suspend 関数の中で並列にする:coroutineScope

async や launch はスコープの中でしか呼べません。suspend 関数の中で並列にしたいときは、coroutineScope で囲みます。

// page.kt
import kotlinx.coroutines.*

suspend fun loadPage(): String = coroutineScope {
    val title = async { delay(300); "タイトル" }
    val body = async { delay(500); "本文" }
    "${title.await()} / ${body.await()}"
}

fun main() = runBlocking {
    val start = System.currentTimeMillis()
    println(loadPage())
    println("所要時間: ${System.currentTimeMillis() - start} ms")
}

実行結果:

タイトル / 本文
所要時間: 514 ms

300ms と 500ms の処理が同時に進み、長いほうの約 500ms で終わっています。coroutineScope は中の子がすべて終わるまで待ち、最後の式を戻り値として返します。

6つの関数の戻り値を実際に見る

早見表の「戻り値」の列を、コードで確かめます。

// returns.kt
import kotlinx.coroutines.*

fun main() = runBlocking {
    println("coroutineScope の戻り値: " + coroutineScope {
        val a = async { delay(100); 1 }
        val b = async { delay(100); 2 }
        a.await() + b.await()
    })
    val job: Job = launch { delay(100) }
    val deferred: Deferred<Int> = async { delay(100); 42 }
    println("launch の戻り値: ${job::class.simpleName}(Job)")
    println("async の戻り値: await() = ${deferred.await()}")
    println("withContext の戻り値: " + withContext(Dispatchers.Default) { "計算結果" })
    println("supervisorScope の戻り値: " + supervisorScope { "ok" })
}

実行結果:

coroutineScope の戻り値: 3
launch の戻り値: StandaloneCoroutine(Job)
async の戻り値: await() = 42
withContext の戻り値: 計算結果
supervisorScope の戻り値: ok

launch と async は「起動するもの」で、Job や Deferred という取っ手を返します。coroutineScope・supervisorScope・withContext は「終わるまで待つもの」で、ブロックの結果そのものを返します。

launch と async の選び方

判断のポイント 選ぶもの
結果の値がいらない(ログの送信、通知など) launch
結果の値がほしい async と await()
複数の結果をまとめてほしい async をリストにして awaitAll()
順番に実行すればよい async を使わず suspend 関数をそのまま呼ぶ

Dispatchers:どのスレッドで動くかをスレッド名で見る

Dispatcher(ディスパッチャ)とは、コルーチンをどのスレッドで動かすかを決める部品です。スレッド名を出力して違いを確かめます。

// dispatchers.kt
import kotlinx.coroutines.*

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

fun main() = runBlocking {
    log("runBlocking の中")
    launch { log("launch(指定なし)") }
    launch(Dispatchers.Default) { log("Dispatchers.Default") }
    launch(Dispatchers.IO) { log("Dispatchers.IO") }
    launch(Dispatchers.Unconfined) { log("Dispatchers.Unconfined") }

    val text = withContext(Dispatchers.IO) {
        log("withContext(Dispatchers.IO) の中")
        "読み込んだ内容"
    }
    log("withContext のあと: $text")
}

実行結果:

[main] runBlocking の中
[DefaultDispatcher-worker-1] Dispatchers.Default
[DefaultDispatcher-worker-2] Dispatchers.IO
[main] Dispatchers.Unconfined
[main] launch(指定なし)
[DefaultDispatcher-worker-3] withContext(Dispatchers.IO) の中
[main] withContext のあと: 読み込んだ内容
指定 出力されたスレッド名 役割
指定なし(runBlocking の中) main 今回の出力では、親の runBlocking と同じスレッドで動いた
Dispatchers.Default DefaultDispatcher-worker-1 launch や async などで、ほかに指定が無いときに使われる既定のディスパッチャ
Dispatchers.IO DefaultDispatcher-worker-2 スレッドを止める入出力の処理を、共有のスレッドプールに任せるためのもの
Dispatchers.Unconfined main 特定のスレッドに縛られず、呼び出したその場で動き始める
Dispatchers.Main (下のエラー文を参照) 画面の部品を扱うメインスレッドに限定されたもの

表の Default・IO・Unconfined・Main の「役割」は、kotlinx.coroutines の API リファレンスの説明を要約したものです。

runBlocking の中で指定なしの launch を使うと、親と同じ main スレッドで動きます。指定なしの launch の出力が4行目より後ろになっているのは、main スレッドが空くのを待っていたためです。Default と IO は別のスレッドなので、先に出力されています。

withContext:スレッドを切り替えて、結果を持って戻る

withContext(Dispatchers.IO) { ... } は、ブロックの中だけ別のスレッドで動かし、終わったら元のスレッドに戻ります。出力の最後の2行で、worker-3 から main に戻っていることがわかります。新しいコルーチンを起動するのではなく、いまのコルーチンの実行場所を一時的に変えるのが launch・async との違いです。

Dispatchers.Main は追加のライブラリが必要

コマンドラインのプログラムで Dispatchers.Main を使うと、次の例外が出ます。

// main_dispatcher.kt
import kotlinx.coroutines.*

fun main() = runBlocking {
    try {
        withContext(Dispatchers.Main) { println("Main") }
    } catch (e: IllegalStateException) {
        println("${e::class.simpleName}: ${e.message}")
    }
}

実行結果:

IllegalStateException: Module with the Main dispatcher is missing. Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android' and ensure it has the same version as 'kotlinx-coroutines-core'

エラー文のとおり、Main ディスパッチャを提供するライブラリを、core と同じバージョンで追加すると使えるようになります。

delay と Thread.sleep の違いを時間で確かめる

// sleep.kt
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

fun main() = runBlocking {
    val t1 = measureTimeMillis {
        coroutineScope {
            repeat(3) { launch { delay(500) } }
        }
    }
    println("delay × 3: $t1 ms")
    val t2 = measureTimeMillis {
        coroutineScope {
            repeat(3) { launch { Thread.sleep(500) } }
        }
    }
    println("Thread.sleep × 3: $t2 ms")
    val t3 = measureTimeMillis {
        coroutineScope {
            repeat(3) { launch(Dispatchers.IO) { Thread.sleep(500) } }
        }
    }
    println("Thread.sleep × 3(Dispatchers.IO): $t3 ms")

    val t4 = measureTimeMillis {
        coroutineScope {
            repeat(100_000) { launch { delay(1000) } }
        }
    }
    println("コルーチン 10万個: $t4 ms")
}

実行結果:

delay × 3: 538 ms
Thread.sleep × 3: 1502 ms
Thread.sleep × 3(Dispatchers.IO): 512 ms
コルーチン 10万個: 2453 ms
書き方 所要時間 理由
delay(500) を3つ 538 ms スレッドを止めないので、1本の main スレッドで3つが同時に待てる
Thread.sleep(500) を3つ 1502 ms main スレッドを止めるので、1つずつ順番になる
Thread.sleep(500) を Dispatchers.IO で3つ 512 ms 別々のスレッドで止まるので、同時に進む
1秒待つコルーチンを10万個 2453 ms 1本のスレッドで10万個を起動して、すべて待ち終えた

スレッドを止める処理(Thread.sleep や、待ち時間のある従来の入出力 API)を呼ぶときは withContext(Dispatchers.IO) で囲むと、ほかのコルーチンを待たせずに済みます。10万個のコルーチンが約2.5秒で終わっていることから、コルーチン1つが軽いこともわかります。

キャンセル:CancellationException と後片付け

起動したコルーチンは、Job の cancel() で止められます。cancelAndJoin() は、キャンセルして終了まで待つ関数です。

// cancel.kt
import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        try {
            repeat(10) { i ->
                println("処理中 $i")
                delay(300)
            }
        } catch (e: CancellationException) {
            println("catch: ${e::class.simpleName}: ${e.message}")
            throw e                       // 受け取ったら投げ直す
        } finally {
            println("finally: 後片付け")
        }
    }
    delay(1000)
    println("キャンセルします")
    job.cancelAndJoin()
    println("isCancelled=${job.isCancelled} isCompleted=${job.isCompleted}")
}

実行結果:

処理中 0
処理中 1
処理中 2
処理中 3
キャンセルします
catch: JobCancellationException: StandaloneCoroutine was cancelled
finally: 後片付け
isCancelled=true isCompleted=true

キャンセルされると、待っていた delay が CancellationException を投げます(出力のクラス名 JobCancellationException はその一種です)。通常の例外と同じように finally が動くので、ファイルを閉じるなどの後片付けを書けます。

CancellationException は握りつぶさずに投げ直す

catch (e: Exception) で例外をまとめて受け取ると、CancellationException も一緒に受け取ることになります。そのまま続けると、キャンセルが効かなくなります。

// swallow.kt : キャンセルが効かなくなる例
import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        repeat(5) { i ->
            try {
                delay(300)
                println("処理 $i")
            } catch (e: Exception) {
                println("例外を握りつぶした: ${e::class.simpleName}")
            }
        }
        println("ループを最後まで回ってしまった")
    }
    delay(500)
    job.cancelAndJoin()
    println("終了")
}

実行結果:

処理 0
例外を握りつぶした: JobCancellationException
例外を握りつぶした: JobCancellationException
例外を握りつぶした: JobCancellationException
例外を握りつぶした: JobCancellationException
ループを最後まで回ってしまった
終了

キャンセルを指示したあとも、ループが最後まで回っています。直し方は2つあります。

直し方 書き方
CancellationException を先に受けて投げ直す catch (e: CancellationException) { throw e } を catch (e: Exception) の前に書く
受け取る例外の型を絞る catch (e: IOException) のように、必要な型だけを書く

計算ループは isActive を確認する

キャンセルは「協調的」です。つまり、コルーチンの側がキャンセルに気づける場所(delay などの中断ポイント)を通ったときに止まります。中断ポイントの無い計算ループは、自分で isActive を確認します。

// active.kt
import kotlinx.coroutines.*

fun main() = runBlocking {
    // 1) 中断ポイントが無い計算ループ: キャンセルを指示しても最後まで回る
    val start1 = System.currentTimeMillis()
    val job1 = launch(Dispatchers.Default) {
        var next = start1
        var i = 0
        while (i < 5) {
            if (System.currentTimeMillis() >= next) {
                println("確認なし: $i")
                i++
                next += 300
            }
        }
    }
    delay(500)
    job1.cancelAndJoin()
    println("job1 終了まで ${System.currentTimeMillis() - start1} ms")

    // 2) isActive を確認するループ: キャンセルですぐ止まる
    val start2 = System.currentTimeMillis()
    val job2 = launch(Dispatchers.Default) {
        var next = start2
        var i = 0
        while (isActive && i < 5) {
            if (System.currentTimeMillis() >= next) {
                println("isActive あり: $i")
                i++
                next += 300
            }
        }
    }
    delay(500)
    job2.cancelAndJoin()
    println("job2 終了まで ${System.currentTimeMillis() - start2} ms")
}

実行結果:

確認なし: 0
確認なし: 1
確認なし: 2
確認なし: 3
確認なし: 4
job1 終了まで 1204 ms
isActive あり: 0
isActive あり: 1
job2 終了まで 503 ms

500ms でキャンセルを指示しましたが、確認なしのループは 1204ms まで動き続けました。while (isActive && ...) に変えると、503ms で止まっています。

タイムアウト:withTimeout と withTimeoutOrNull

「一定時間で終わらなければ打ち切る」処理は、withTimeout で書けます。

// timeout.kt
import kotlinx.coroutines.*

suspend fun slow(): String {
    delay(2000)
    return "完了"
}

fun main() = runBlocking {
    try {
        val r = withTimeout(500) { slow() }
        println(r)
    } catch (e: TimeoutCancellationException) {
        println("withTimeout: ${e::class.simpleName}: ${e.message}")
    }

    val r2 = withTimeoutOrNull(500) { slow() }
    println("withTimeoutOrNull: $r2")

    val r3 = withTimeoutOrNull(3000) { slow() }
    println("withTimeoutOrNull(間に合う): $r3")
}

実行結果:

withTimeout: TimeoutCancellationException: Timed out waiting for 500 ms
withTimeoutOrNull: null
withTimeoutOrNull(間に合う): 完了
関数 時間内に終わったとき 時間を超えたとき 向いている場面
withTimeout(ms) ブロックの結果 TimeoutCancellationException を投げる 時間切れをエラーとして扱いたい
withTimeoutOrNull(ms) ブロックの結果 null を返す 時間切れなら既定値を使いたい(?: 既定値 と組み合わせる)

例外:launch と async で出るタイミングが違う

公式ドキュメントの「Coroutine exceptions handling」には、コルーチンを起動する関数には2種類あり、launch は例外を自動で伝え、async は利用者に見せる形で持つと書かれています。実際の動きを確かめます。

// exceptions.kt
import kotlinx.coroutines.*

fun main() = runBlocking {
    // supervisorScope の中で試すと、子の例外が親(runBlocking)に広がらない
    supervisorScope {
        val handler = CoroutineExceptionHandler { _, e ->
            println("[handler] launch の例外を受け取った: $e")
        }
        println("1) launch を起動")
        val job = launch(handler) {
            delay(100)
            throw IllegalStateException("launch の中で発生")
        }
        job.join()
        println("   join() は例外を投げない。isCancelled=${job.isCancelled}")

        println("2) async を起動")
        val deferred = async {
            delay(100)
            throw IllegalStateException("async の中で発生")
        }
        delay(500)
        println("   500ms 待っても何も出ない。isCancelled=${deferred.isCancelled}")
        try {
            deferred.await()
        } catch (e: IllegalStateException) {
            println("   await() で受け取った: $e")
        }
    }
    println("最後まで到達")
}

実行結果:

1) launch を起動
[handler] launch の例外を受け取った: java.lang.IllegalStateException: launch の中で発生
   join() は例外を投げない。isCancelled=true
2) async を起動
   500ms 待っても何も出ない。isCancelled=true
   await() で受け取った: java.lang.IllegalStateException: async の中で発生
最後まで到達
関数 例外が出るタイミング 受け取り方
launch 例外が起きた時点で、すぐ親やハンドラへ伝わる 中で try-catch する。または CoroutineExceptionHandler を用意する
async await() を呼んだ時点 await() を try-catch で囲む

async の中では 100ms の時点で例外が起きていますが、500ms 待っても何も出力されず、await() を呼んだところで初めて受け取れました。

launch を try で囲んでも受け取れない

launch はコルーチンを起動しただけで先へ進むので、外側の try-catch では受け取れません。

// trycatch.kt
import kotlinx.coroutines.*

fun main() {
    // withContext: 中の例外は、その場で呼び出し元に届く
    runBlocking {
        try {
            withContext(Dispatchers.Default) {
                throw IllegalStateException("withContext の中で発生")
            }
        } catch (e: IllegalStateException) {
            println("withContext を try で囲んで受け取った: ${e.message}")
        }
    }

    // launch を try で囲んでも受け取れない(起動しただけで先へ進むため)
    try {
        runBlocking {
            try {
                launch { throw IllegalStateException("launch の中で発生") }
            } catch (e: IllegalStateException) {
                println("この行は出力されない")
            }
        }
    } catch (e: IllegalStateException) {
        println("runBlocking の外まで届いた: ${e.message}")
    }
}

実行結果:

withContext を try で囲んで受け取った: withContext の中で発生
runBlocking の外まで届いた: launch の中で発生

launch の例外は親の runBlocking に伝わり、runBlocking を呼んだ場所で投げられました。どこでも受け取らない次のコードでは、プログラムはスタックトレースを出して終了します。

// c8.kt
import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        delay(100)
        throw IllegalStateException("launch の中で発生")
    }
    job.join()
    println("ここには到達しない")
}

実行結果(先頭の3行。終了コードは 1):

Exception in thread "main" java.lang.IllegalStateException: launch の中で発生
    at C8Kt$main$1$job$1.invokeSuspend(c8.kt:6)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:34)

coroutineScope と supervisorScope:エラーの広がり方の違い

子を2つ起動し、片方(A)を 300ms でエラーにします。もう片方(B)がどうなるかを比べます。

// scopes.kt
import kotlinx.coroutines.*

suspend fun task(name: String, ms: Long, fail: Boolean = false): String {
    try {
        delay(ms)
        if (fail) throw IllegalArgumentException("$name でエラー")
        println("  $name 完了")
        return name
    } catch (e: CancellationException) {
        println("  $name はキャンセルされた")
        throw e
    }
}

fun main() = runBlocking {
    println("--- coroutineScope: 1つがエラーになると残りもキャンセル")
    try {
        coroutineScope {
            launch { task("A", 300, fail = true) }
            launch { task("B", 1000) }
        }
    } catch (e: IllegalArgumentException) {
        println("  呼び出し元で catch: ${e.message}")
    }

    println("--- supervisorScope: ほかの子はそのまま続く")
    supervisorScope {
        val a = async { task("A", 300, fail = true) }
        val b = async { task("B", 1000) }
        val ra = runCatching { a.await() }
        println("  A の結果: ${ra.exceptionOrNull()?.message}")
        println("  B の結果: ${b.await()}")
    }
}

実行結果:

--- coroutineScope: 1つがエラーになると残りもキャンセル
  B はキャンセルされた
  呼び出し元で catch: A でエラー
--- supervisorScope: ほかの子はそのまま続く
  A の結果: A でエラー
  B 完了
  B の結果: B
スコープ A がエラーになったとき B 呼び出し元
coroutineScope スコープ全体がキャンセルされる キャンセルされる coroutineScope を囲んだ try-catch で受け取れる
supervisorScope A だけが終了する 最後まで動く 子ごとに結果を確認する

親子でまとめて管理し、1つがエラーになったら残りも止めるこの仕組みを、構造化された並行性(structured concurrency)と呼びます。起動したまま忘れられたコルーチンが残らないのが利点です。

判断のポイント 選ぶもの
全部そろって初めて意味がある(1つ欠けたらやり直す) coroutineScope
取れたものだけ使えばよい(一覧の一部だけ表示するなど) supervisorScope

つまずきやすい点のチェックリスト

症状 原因 対処
suspend function ... can only be called from a coroutine ふつうの関数から suspend 関数を呼んだ runBlocking の中、または suspend 関数の中から呼ぶ
unresolved reference 'launch' スコープの外で launch を呼んだ coroutineScope { } で囲む
async にしたのに速くならない 起動してすぐ await() している 先に全部起動してから await() / awaitAll()
launch を try で囲んでも例外を受け取れない launch は起動だけして先へ進む launch の中で try-catch する
キャンセルしても止まらない CancellationException を握りつぶしている、または中断ポイントが無い 投げ直す、isActive を確認する
ほかのコルーチンが進まない Thread.sleep などスレッドを止める処理を呼んでいる delay を使う、または withContext(Dispatchers.IO) で囲む
Module with the Main dispatcher is missing Main ディスパッチャを提供するライブラリが無い エラー文の案内どおり、同じバージョンのライブラリを追加する

よくある質問

Q. runBlocking はいつ使いますか? A. main 関数やテストなど、ふつうのコードからコルーチンの世界に入る入口で使います。スレッドごと止めて待つ関数なので、コルーチンの中から呼ぶ用途には coroutineScope や withContext が向いています。

Q. launch と async はどう違いますか? A. launch は結果のいらない処理を起動して Job を返し、async は結果のいる処理を起動して Deferred を返します。例外の出るタイミングも違い、launch は起きた時点で親へ伝わり、async は await() を呼んだ時点で投げられます。

Q. coroutineScope と withContext はどう使い分けますか? A. どちらもブロックが終わるまで中断して待ち、結果を返します。複数の子を並列に起動してまとめたいときは coroutineScope、実行するスレッド(Dispatcher)を切り替えたいときは withContext を使います。

Q. コルーチンとスレッドは何が違いますか? A. コルーチンはスレッドの上で動く、より軽い実行単位です。この記事の計測では、1秒待つコルーチンを10万個起動しても、main スレッド1本で約2.5秒で完了しました。待っている間はスレッドを手放すので、少ないスレッドで多くの処理を同時に進められます。

まとめ

  • runBlocking は入口、launch と async は起動、coroutineScope・supervisorScope・withContext は終わるまで待って結果を返す
  • suspend 関数を並べると直列(2023 ms)、async で包んで await すると並列(1009 ms)。複数まとめるときは awaitAll
  • Dispatchers はスレッドの選び方。スレッドを止める処理は withContext(Dispatchers.IO) で囲む
  • キャンセルは CancellationException で伝わる。握りつぶさずに投げ直し、計算ループは isActive を確認する
  • withTimeout は TimeoutCancellationException を投げ、withTimeoutOrNull は null を返す
  • launch の例外はすぐ親へ、async の例外は await() のときに出る
  • coroutineScope は1つのエラーで残りもキャンセル、supervisorScope はほかの子が続く

参考・出典

  • kotlinx.coroutines 公式リポジトリ README(Gradle の依存、対応する Kotlin の版): https://github.com/Kotlin/kotlinx.coroutines/blob/master/README.md
  • Kotlin 公式ドキュメント「Coroutine exceptions handling」: https://kotlinlang.org/docs/exception-handling.html
  • kotlinx.coroutines API リファレンス「Dispatchers」: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Kotlinコルーチン入門|launch・async・runBlocking」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?