はじめに
Kotlin のコルーチンでは、キャンセル(Cancellation) は非常に重要な概念です。
非同期処理を中断し、リソースを安全に解放することで、アプリ全体の安定性を確保します。
この記事では以下の内容を体系的に紹介します:
- コルーチンキャンセルの基本構造
-
Jobとキャンセル伝播の仕組み -
isActive/ensureActive()の使い方 -
withTimeout()/withTimeoutOrNull() -
try-finallyとNonCancellableによる後処理 -
SupervisorScopeとの関係
1. コルーチンキャンセルの基本
KotlinのCoroutineScope.launchで起動されたコルーチンは、Jobオブジェクトを返します。
Jobを通じてキャンセル制御が行われます。
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
repeat(1000) { i ->
println("Job: I'm working $i ...")
delay(500L)
}
}
delay(1300L)
println("Main: Tired of waiting! Cancelling...")
job.cancel() // キャンセル要求
job.join() // 終了待機
println("Main: Done.")
}
ポイント:
-
cancel()はキャンセルリクエストを送るだけ - コルーチンは協調的に(cooperative)キャンセルされる
-
delay()やyield()などの中断可能関数(suspending function) がキャンセルを検知して停止する
2. 協調的キャンセルの仕組み
すべてのコルーチンが即時に停止するわけではありません。
CPU計算のように delay() を使わない処理では、自らキャンセル状態をチェックする必要があります。
fun main() = runBlocking {
val job = launch(Dispatchers.Default) {
var i = 0
while (isActive) { // ← 協調的キャンセル
println("Working ${i++} ...")
}
}
delay(100L)
println("Cancel!")
job.cancelAndJoin()
}
isActive
-
CoroutineScopeの拡張プロパティ - 現在のコルーチンがアクティブかどうかを表す
代わりに ensureActive() を使うと、キャンセル状態なら CancellationException を即時投げます。
3. タイムアウトによるキャンセル
withTimeout
Kotlin には一定時間後に自動キャンセルする仕組みも用意されています。
import kotlinx.coroutines.*
fun main() = runBlocking {
try {
withTimeout(1300L) {
repeat(1000) { i ->
println("Job: $i ...")
delay(500L)
}
}
} catch (e: TimeoutCancellationException) {
println("Timed out!")
}
}
withTimeoutOrNull
例外を発生させず、タイムアウト時に null を返します。
val result = withTimeoutOrNull(1000L) {
delay(1500L)
"Finished"
}
println(result) // => null
4. 後処理とキャンセル不可領域
キャンセル時にも必ず後処理が必要な場合は、try-finally を使います。
val job = launch {
try {
repeat(1000) { i ->
println("Job: $i ...")
delay(500L)
}
} finally {
println("Job: Cleaning up resources...")
}
}
ただし、finally で呼ばれる処理もキャンセル可能です。
確実に実行したい場合は、withContext(NonCancellable) を使用します。
finally {
withContext(NonCancellable) {
println("Running cleanup even if cancelled...")
delay(1000L)
println("Cleanup finished.")
}
}
5. キャンセル伝播と SupervisorScope
構造化並行性により、親スコープがキャンセルされると子コルーチンも連鎖的にキャンセルされます。
fun main() = runBlocking {
val parent = launch {
launch {
try {
repeat(1000) { i ->
println("Child: $i ...")
delay(500L)
}
} finally {
println("Child: cancelled.")
}
}
}
delay(1300L)
println("Parent cancelling...")
parent.cancelAndJoin()
println("Done.")
}
ただし、SupervisorScope を使うと子の失敗が親へ伝播しません。
supervisorScope {
launch {
throw RuntimeException("Failure in child")
}
launch {
delay(1000)
println("Another child still running")
}
}
まとめ
| 概念 | 説明 |
|---|---|
cancel() |
キャンセル要求を送る(協調的) |
isActive / ensureActive()
|
キャンセル状態を明示的に確認 |
withTimeout() |
タイムアウト自動キャンセル |
try-finally |
リソース解放 |
NonCancellable |
キャンセル不可の後処理領域 |
SupervisorScope |
子の失敗が親へ伝播しない |