0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Kotlin】Kotlin Coroutinesでのキャンセル伝播とJobの仕組み

Posted at

はじめに

― 「止めたい時に、確実に止める」非同期設計の核心 ―


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がない
    }
}

対策

  1. 明示的に yield() を挿入
  2. または 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を組み合わせる

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?