[Kotlin]coroutines の SupervisorJob の役割とは何なのか


はじめに 

皆さん coroutines は使っていますでしょうか? coroutines を使っていると SupervisorJob を使うことってありますよね?そのときとりあえず SupervisorJob を付けてしまったという経験はないでしょうか、私は言われるがままに付けてしまったことがあります。SupervisorJob をなんとなく使い続けるのはやばいと思ってきたので、今回は SupervisorJob について調べてどんな役割があるのかまとめたいと思います。

SupervisorJob とは

SupervisorJob - kotlinx-coroutines-core を確認したところ SupervisorJob には以下の役割があるそうです。

- SupervisorJob の子 Job は独立して失敗するようになる。
- つまり子 Job でエラーが発生してた場合に親 Job である SupervisorJob がキャンセルされない動作になる。

SupervisorJob を使わない場合は通常の Job で処理が実行されます、なので Job A でもしエラーが発生した場合は Parent Job にエラーが伝搬されて最終的には Job B もキャンセルされる動作になります。

SupervisorJob を使うことで子 Job で発生したエラーは親 Job に伝搬されないような動作になるということです。

SupervisorJob の動作を確認する

SupervisorJob の役割がわかったところで動作を確認していきます。
まずはじめに通常の Job を使った場合について動作確認してみます。

/**
 * 通常の Job を持った CoroutineScope で起動した場合
 */
private fun launchOnDefaultJobScope() = runBlocking {
    // Job を指定していない場合は CoroutineScope 初期化時に Job を設定してくれるようになっている。
    // なので特に Job を指定しない場合には通常の Job で動作するようになる。
    val scope = CoroutineScope(Dispatchers.Default)

    scope.launch {
        throw Exception("GOOD ERROR MESSAGE!!")
    }

    scope.launch {
        println("GOOD PRINT")
    }

    delay(1000)
}

結果は以下の通りで特定の子 Job でエラーが発生すると、その他の Job もキャンセルされてしまいます。

Exception in thread "DefaultDispatcher-worker-1" java.lang.Exception: GOOD ERROR MESSAGE!!
    at SampleKt$launchOnDefaultJobScope$1$1.invokeSuspend(Sample.kt:20)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
    at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)

Process finished with exit code 0

次に SupervisorJob を使った場合について動作確認してみましょう。

/**
 * SupervisorJob を持った CoroutineScope で起動した場合
 */
private fun launchOnSupervisorJobScope() = runBlocking {
    // SupervisorJob を設定した場合は子の Job のエラーが他の Job に伝搬しないようになる
    val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

    scope.launch {
        throw Exception("GOOD ERROR MESSAGE!!")
    }

    scope.launch {
        println("GOOD PRINT")
    }

    delay(1000)
}

結果は以下の通りで SupervisorJob を使った場合だと子 Job でエラーが発生しても、
その他の Job はキャンセルされずに実行できるようになります。

GOOD PRINT
Exception in thread "DefaultDispatcher-worker-1" java.lang.Exception: GOOD ERROR MESSAGE!!
    at SampleKt$launchOnSupervisorJobScope$1$1.invokeSuspend(Sample.kt:67)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
    at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)

なぜ SupervisorJob が必要になるのか?

SupervisorJob を利用するとエラーが伝搬されなくなるので、それ自体にメリットがあるのですが特に Async で Deferred(Job)を作成するときに有効です。次のように Async で Deffered (Job) を作成して、await で完了するまで待つ処理を記述したとします。もしこの処理で例外が発生したら try-catch で処理することになると思います。

try-catch で例外を処理しているので問題なく動作しているように見えるのですが、
SupervisorJob を使わない場合ですと try-catch しても他の子 Job がキャンセルされてしまいます。

/**
 * 通常の Job を持った CoroutineScope で async を使って起動した場合
 */
private fun launchAsyncOnDefaultJobScope() = runBlocking {
    // Job を指定していない場合は CoroutineScope 初期化時に JOB を設定してくれるようになっている。
    // 通常の JOB ではある子が失敗したら他の子に失敗が連鎖し動作がとまってしまうらしい
    val scope = CoroutineScope(Dispatchers.Default)

    // async を使って await したときに例外した場合には以下のように try catch で wrapping すると例外が拾える
    // これで問題ないように見えるが async は1つの Job になるので、ここで発生した例外はすべての 子 JOB に影響してしまう。
    scope.launch {
        try {
            async {
                delay(100)
                throw Exception("DEFFERED ERROR MESSAGE!!")
            }.await()
        } catch (e: Exception) {
            println(e.message)
        }
    }

    scope.launch {
        delay(200)
        println("GOOD PRINT")
    }

    delay(1000)
}
DEFFERED ERROR MESSAGE!!
Exception in thread "DefaultDispatcher-worker-2" java.lang.Exception: DEFFERED ERROR MESSAGE!!
    at SampleKt$launchAsyncOnDefaultJobScope$1$1$1.invokeSuspend(Sample.kt:44)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
    at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)

SupervisorJob を使う場合ですと子 Job の失敗は、他の子 Job に伝搬しなくなります。
なので Async で Deffered (Job) を作成して、await で完了まで待つ処理を記述する場合でも安全に処理できます。

/**
 * async で発生した例外を他の Job に伝搬させないために async 内で例外を catch する
 */
private fun launchSafeAsync() = runBlocking {
    val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

    scope.launch {
        async {
            try {
                delay(100)
                throw Exception("DEFFERED ERROR MESSAGE!!")
            } catch (e: Exception) {
                println(e.message)
            }
        }.await()
    }

    scope.launch {
        delay(200)
        println("GOOD PRINT")
    }

    delay(1000)
}
DEFFERED ERROR MESSAGE!!
Exception in thread "DefaultDispatcher-worker-3" java.lang.Exception: DEFFERED ERROR MESSAGE!!
    at SampleKt$launchAsyncOnSupervisorScope$1$1$1.invokeSuspend(Sample.kt:90)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
    at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
GOOD PRINT

他にも SupervisorJob が約立つことはありますが、主にこのような場面でエラーを伝搬させたくない場合に使えるようです。

おわりに

今回は SupervisorJob の役割について調べてまとめてみました。SupervisorJob はある子 Job のエラーを他の子 Job に伝搬させない特徴があり便利ですね。ですが SupervisorJob だから例外処理をやらなくてもいいということにはならないと思うのでそこは注意して使う必要がありますね。

参考文献