- Kotlin
Kotlinコルーチン入門|launch・async・runBlocking
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期連続の黒字決算。