アプリ開発備忘録

PlayStationMobile、Android、UWPの開発備忘録

SupervisorJobは基本必要が無いのではないかと思っている

はじめに

以下のコードではtry-catch構文は使用せず、runCatchingを使用しています。文章の中で出てくるrunCatchingはtry-catchと置き換えてもらって大丈夫です。

SupervisorJobを使用しないコード

以下のコードは特にエラーもなく動きます。

withContext

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext

fun main() {
    runBlocking {
        runCatching {
            withContext(Dispatchers.IO) {
                throw Error()
            }
        }
        println("runBlocking tail")
    }

    println("exit")
}
runBlocking tail
exit

async-await

async-awaitを使用するコードに置き換えた以下のコードはエラーが吐かれます。catchされません。

fun main() {
    runBlocking {
        runCatching {
            async(Dispatchers.IO) {
                throw Error()
            }.await()
        }
        println("runBlocking tail")
    }

    println("exit")
}
runBlocking tail
Exception in thread "main" java.lang.Error

async-awaitとエラーハンドリング

このような動作になっている理由として考えられるのが、どこでキャッチすればいいかが不明瞭という点があると思います。 asyncの時点で動作は開始していますが、処理の待受は別の場所でできます。
なのでここでの動作としては runBlocking がエラーをthrowします。

runBlocking {
    val task = async(Dispatchers.IO) { throw Error() } // ここで実行開始しているからここでthrow?
    runCatching {
        // val task = async(Dispatchers.IO) { throw Error() } // ここで定義したとしたら?
        task.await() // ここでawaitしているからここでthrow?
    }
    println("runBlocking tail")
}

中でエラーハンドリングをすれば問題なく動作します。

fun main() {
    runBlocking {
        async(Dispatchers.IO) {
            runCatching { throw Error() }
        }.await()
        println("runBlocking tail")
    }

    println("exit")
}
runBlocking tail
exit

SupervisorJobを使用する例

これだけでいいのに、何故か SupervisorJob を使うような例が多く見受けられます。
例としてこのようなものになります。async を囲んでキャッチしています。
エラーとして出力はされますが、delayされた方の処理は実行されています。

fun main() {
    runBlocking {
        val scope = CoroutineScope(SupervisorJob())

        scope.launch {
            runCatching {
                async {
                    throw Error()
                }.await()
            }
        }

        scope.launch {
            delay(200)
            println("launch 1")
        }

        delay(1000)

        println("runBlocking tail")
    }
}
Exception in thread "DefaultDispatcher-worker-2" java.lang.Error
launch 1
runBlocking tail

SupervisorJobは必要か

上記のSupervisorJobを使う例はただ、asyncの中でキャッチされない例外を握りつぶしているだけに見えます。
エラーが起きるとわかっている部分で早期にキャッチするべきであって、このようなSupervisorJobの使い方は適切でないと考えています。

一番最初に書いたこの例も問題はありませんが、withContextの中でエラーをハンドリングすべきだと考えます。

// 良くない?
runCatching {
    withContext(Dispatchers.IO) {
        throw Error()
    }
}

// OK
withContext(Dispatchers.IO) {
    runCatching {
        throw Error()
    }
}

おわりに

基本的にSupervisorJobを使うことは無いのではないかと思っています。
適切に、そして早期にエラーをハンドリングをしましょう。
そしてasyncをrunCatchingで囲む事はやめましょう。上記の、どこでエラーがthrowされるのかが不明瞭という点を意識すれば、適切でないことはわかりやすいでしょう。