はじめに
― 「止めたい時に、確実に止める」非同期設計の核心 ―
1. Jobとは?
Job は コルーチンのライフサイクルを管理するハンドル(制御句) です。
役割
- コルーチンの開始・完了・キャンセル状態を追跡する
- 親子関係(構造化並行性) の中で、状態を伝播する
-
join(),cancel(),isActiveなどを使って制御できる
Jobのライフサイクル
| 状態 | 説明 |
|---|---|
| New | まだ開始していない |
| Active | 実行中 |
| Cancelling | キャンセル要求中 |
| Cancelled | キャンセル完了 |
| Completed | 正常終了 |
2. Jobを取得して状態を確認する
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
repeat(3) {
println("Task $it 実行中...")
delay(300)
}
}
delay(500)
println("Jobの状態: ${job.isActive}, ${job.isCancelled}, ${job.isCompleted}")
}
🧾 出力例:
Task 0 実行中...
Task 1 実行中...
Jobの状態: true, false, false
Jobは常にコルーチンの現在状態を示します。
状況に応じてcancel()で明示的に停止できます。
3. コルーチンのキャンセルとは?
Kotlinのキャンセルは「協調的キャンセル(Cooperative Cancellation)」です。
コルーチンは、自分がキャンセルされたかを確認して、自発的に止まる必要があります。
自動停止しない例(Bad)
fun main() = runBlocking {
val job = launch {
while (true) { // 無限ループ
println("動作中...")
}
}
delay(1000)
job.cancel() // 無視される
println("キャンセル要求済み")
}
🧨 出力:
動作中...(止まらない!)
cancel()は「要求を出す」だけ。
実際に止めるには、コルーチン側がキャンセルを意識する必要があります。
協調的キャンセル(Good)
fun main() = runBlocking {
val job = launch {
while (isActive) {
println("動作中...")
delay(200)
}
}
delay(600)
job.cancelAndJoin()
println("キャンセル完了")
}
出力:
動作中...
動作中...
キャンセル完了
isActiveまたはdelay()を通してキャンセルを検知できます。
4. cancel() と cancelAndJoin()
| メソッド | 説明 |
|---|---|
cancel() |
キャンセル要求を送る(非同期) |
join() |
コルーチンの完了を待つ |
cancelAndJoin() |
両方を一度に行う(推奨) |
val job = launch { ... }
job.cancelAndJoin() // → 安全で確実に停止
5. 親子スコープでのキャンセル伝播
親から子へ伝播する
fun main() = runBlocking {
val parent = launch {
launch {
repeat(10) {
println("子タスク実行中...")
delay(100)
}
}
}
delay(300)
println("親キャンセル!")
parent.cancel()
delay(200)
}
出力:
子タスク実行中...
子タスク実行中...
子タスク実行中...
親キャンセル!
parent.cancel()により、子コルーチンも自動で停止します。
子から親には伝播しない(Supervisor使用時)
fun main() = runBlocking {
val scope = CoroutineScope(SupervisorJob())
scope.launch {
launch {
throw Exception("子で失敗")
}
launch {
delay(500)
println("他の子は継続")
}
}
delay(1000)
println("完了")
}
出力:
Exception: 子で失敗
他の子は継続
完了
SupervisorJobにより、子の失敗が親に伝播しません。
6. キャンセルが伝わらないケース
キャンセルは「中断ポイント(suspension point)」でのみ伝わります。
つまり、delay() や yield() がないと止まりません。
❌ キャンセルされない例
launch {
while (isActive) {
// CPUバウンドでdelayがない
}
}
対策
- 明示的に
yield()を挿入 - または
ensureActive()を定期的に呼ぶ
while (true) {
ensureActive() // キャンセルを検知
}
7. Jobの階層構造を図で理解する
- 親が
cancel()されると、すべての子に伝播 - ただし、SupervisorJob を使うと独立性を確保できる
8. Jobの監視(joinAll / cancelChildren)
val parent = launch {
launch { delay(1000); println("A 完了") }
launch { delay(2000); println("B 完了") }
}
delay(500)
parent.cancelChildren() // 子のみ停止、親は存続
cancelChildren()は親スコープを残して、
子だけをキャンセルする便利メソッドです。
9. ベストプラクティスまとめ
| シーン | 推奨方法 |
|---|---|
| 処理を確実に止めたい | cancelAndJoin() |
| 複数ジョブをまとめて管理 |
joinAll() / cancelChildren()
|
| 子の失敗を無視して継続 |
SupervisorJob / supervisorScope
|
| CPUバウンド処理 |
ensureActive() / yield() でキャンセル検知 |
| UIスコープ(Android) |
viewModelScope が自動でキャンセル伝播 |
まとめ
| 要点 | 内容 |
|---|---|
Job |
コルーチンのライフサイクル制御クラス |
| キャンセル伝播 | 親→子に伝わる(Supervisorで分離可能) |
| 協調的キャンセル | コルーチンが自らキャンセルを検知して停止 |
| 停止メソッド |
cancel(), join(), cancelAndJoin()
|
| 中断ポイント |
delay(), yield(), ensureActive()
|
| 実務Tips | ViewModelScopeやSupervisorJobを組み合わせる |