みなさんはKotlin Coroutines上で時間のかかる処理を行う際、処理のキャンセルについて意識されてますでしょうか?
この記事では、Coroutine上で動かす時間のかかるコード実行を適切にキャンセルするにはwithContextではなくrunInterruptibleを利用するべきであるという例を紹介します。
時間のかかるコード実行をキャンセルさせる例として以下のようなコードを用意しました。
runBlocking {
val heavyWork = fun(): String {
println("Start heavy work")
repeat(10) {
Thread.sleep(1000)
println("Progress ${it + 1}")
}
return "Success"
}
val result = withTimeoutOrNull(5000) {
withContext(Dispatchers.IO) {
heavyWork()
}
}
println("Result: $result")
}
時間のかかる処理のシミュレートとしてheavyWork
を定義しました。このメソッドの完了に10秒掛かります。
厳密にメソッドを10秒間ブロックするため、delay
等のCoroutineの機能を利用せずに非Coroutineで実装していることが時間のかかる処理を再現する上でのポイントです。
このメソッドをCoroutine上で実行しますが、withTimeoutOrNull
によって5秒のタイムアウトを設定しているため、実際にはProgress 5
以降は出力されないことを意図しています。
しかし、出力結果は以下の通りになります。
Start heavy work
Progress 1
Progress 2
Progress 3
Progress 4
Progress 5
Progress 6
Progress 7
Progress 8
Progress 9
Progress 10
Result: null
なぜタイムアウト時点で処理が停止せずProgress 10
まで出力されたのかというと、非Coroutineなメソッドの実行中は途中でCancellationException
をthrowするタイミングが存在しないため、呼び出し側のCoroutineがキャンセルされた後も意図せず処理が続行されてしまいます。
そこでrunInterruptibleを利用します。
時間の掛かる処理の実行時に使っていたwithContext
をrunInterruptible
に入れ替えることによって、この問題は解消します。
runBlocking {
val heavyWork = fun(): String {
println("Start heavy work")
repeat(10) {
Thread.sleep(1000)
println("Progress ${it + 1}")
}
return "Success"
}
val result = withTimeoutOrNull(5000) {
runInterruptible(Dispatchers.IO) {
heavyWork()
}
}
println("Result: $result")
}
このコードの出力結果は以下の通りになり、タイムアウト後ただちに処理が中断されていることが分かります。
Start heavy work
Progress 1
Progress 2
Progress 3
Progress 4
Result: null
以上、ブロックの実行が途中であってもCoroutineのキャンセル時にCancellationExceptionをthrowし、適切に処理をキャンセルできるrunInterruptibleの紹介でした。