Kotlin 1.3: コルーチンでの例外の扱い方 (1/2) からの続きです。
キャンセルと例外
キャンセルは例外と強い関係があります。 コルーチンはキャンセルの際に、 内部的に CoroutineException
を使っています。 この例外は全ての例外ハンドラに無視されるので、 catch
ブロック で利用可能な 付加的なデバッグ情報のためにしか使われません。 コルーチンが 原因なく Job.cancel
によってキャンセルされる時、 そのコルーチンは消滅しますが、 親のコルーチンは消滅しません。 例外の原因を発生させずにキャンセルする機構は、 親のコルーチンが、 自分自身をキャンセルすることなく子のコルーチンを取り消すための仕組みです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
fun main(args: Array<String>) = runBlocking { val job = launch { val child = launch { try { delay(Long.MAX_VALUE) } finally { println("Child is cancelled") } } yield() println("Cancelling child") child.cancel() child.join() yield() println("Parent is not cancelled") } job.join() } |
このコードの出力は次のようになります。
1 2 |
Cancelling child Parent is not cancelled |
Kotlin のドキュメントでは、 "Child is cancelled"
も出力されるよう書かれていますが、 実際に試したところでは出力されませんでした。
コルーチンが CancellationException
以外の例外を検出した場合、 その例外によって親のコルーチンを取り消します。 この動作はオーバーライドすることはできず、 CoroutineExceptionHandler
の実装に依存しない、 構造化された並行性のための安定したコルーチン階層を提供するために使用されます。 元の例外は、すべての子コルーチンが終了したときに親のコルーチンによって処理されます。
次の例で CoroutineExceptionHandler
を常に GlobalScope
で作成されたコルーチンにインストールしている理由は次の通りです。 メインのコルーチンは、インストールされたハンドラに関係なく、子コルーチンが例外で完了したときに常にキャンセルされるためです。 メインの runBlocking
のスコープで起動されるコルーチンに例外ハンドラを設置してもキャンセルを止めることはできません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
fun main(args: Array<String>) = runBlocking { val handler = CoroutineExceptionHandler { _, exception -> println("Caught $exception") } val job = GlobalScope.launch(handler) { launch { // the first child try { delay(Long.MAX_VALUE) } finally { withContext(NonCancellable) { println("Children are cancelled, but exception is not handled until all children terminate") delay(100) println("The first child finished its non cancellable block") } } } launch { // the second child delay(10) println("Second child throws an exception") throw ArithmeticException() } } job.join() } |
このコードの出力は次のようになります。
1 2 3 4 |
Second child throws an exception Children are cancelled, but exception is not handled until all children terminate The first child finished its non cancellable block Caught java.lang.ArithmeticException |
親のコルーチンで、子のコルーチンを2つ起動しています。 子の一方で例外が発生した時、もう一方の子が terminate されます。 そして、 finally
の処理が終わると ようやく親のコルーチンに例外が伝播します。
例外が発生していない方の子コルーチンでは、どんな例外が出ているのでしょうか。 catch
ブロック を記述して内容を見てみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
fun main(args: Array<String>) = runBlocking { val handler = CoroutineExceptionHandler { _, exception -> println("Caught $exception") } val childHandler = CoroutineExceptionHandler { _, exception -> println(exception.message) exception.printStackTrace() } val job = GlobalScope.launch(handler) { launch(childHandler) { // the first child try { delay(Long.MAX_VALUE) } catch (e: CancellationException) { println(e.message) e.printStackTrace() } catch (e: Exception) { println(e.message) e.printStackTrace() } finally { withContext(NonCancellable) { println("Children are cancelled, but exception is not handled until all children terminate") delay(100) println("The first child finished its non cancellable block") } } } launch(childHandler) { // the second child delay(10) println("Second child throws an exception") throw ArithmeticException() } } job.join() } |
出力は次のようになります。 JobCancellationException
が出ていることがわかります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
Second child throws an exception Parent job is Cancelling kotlinx.coroutines.experimental.JobCancellationException: Parent job is Cancelling; job="coroutine#2":StandaloneCoroutine{Cancelling}@4f3a53db at kotlinx.coroutines.experimental.JobSupport.getChildJobCancellationCause(JobSupport.kt:633) at kotlinx.coroutines.experimental.JobSupport.createCauseException(JobSupport.kt:642) at kotlinx.coroutines.experimental.JobSupport.makeCancelling(JobSupport.kt:669) at kotlinx.coroutines.experimental.JobSupport.cancelImpl(JobSupport.kt:596) at kotlinx.coroutines.experimental.JobSupport.parentCancelled(JobSupport.kt:580) at kotlinx.coroutines.experimental.ChildHandleNode.invoke(JobSupport.kt:1316) at kotlinx.coroutines.experimental.JobSupport.notifyCancelling(JobSupport.kt:1346) at kotlinx.coroutines.experimental.JobSupport.makeCancelling(JobSupport.kt:664) at kotlinx.coroutines.experimental.JobSupport.cancelImpl(JobSupport.kt:596) at kotlinx.coroutines.experimental.JobSupport.childCancelled(JobSupport.kt:585) at kotlinx.coroutines.experimental.ChildHandleNode.childCancelled(JobSupport.kt:1317) at kotlinx.coroutines.experimental.JobSupport.cancelParent(JobSupport.kt:926) at kotlinx.coroutines.experimental.JobSupport.notifyCancelling(JobSupport.kt:304) at kotlinx.coroutines.experimental.JobSupport.tryMakeCompleting(JobSupport.kt:794) at kotlinx.coroutines.experimental.JobSupport.makeCompletingOnce$kotlinx_coroutines_core(JobSupport.kt:743) at kotlinx.coroutines.experimental.AbstractCoroutine.resumeWithException(AbstractCoroutine.kt:123) at kotlin.coroutines.experimental.migration.ContinuationMigration.resumeWith(CoroutinesMigration.kt:85) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:45) at kotlin.coroutines.experimental.migration.ExperimentalContinuationMigration.resume(CoroutinesMigration.kt:76) at kotlinx.coroutines.experimental.DispatchedKt.resumeCancellable(Dispatched.kt:118) at kotlinx.coroutines.experimental.ResumeModeKt.resumeMode(ResumeMode.kt:22) at kotlinx.coroutines.experimental.DispatchedKt.dispatch(Dispatched.kt:201) at kotlinx.coroutines.experimental.AbstractContinuation.dispatchResume(AbstractContinuation.kt:185) at kotlinx.coroutines.experimental.AbstractContinuation.completeStateUpdate(AbstractContinuation.kt:253) at kotlinx.coroutines.experimental.AbstractContinuation.updateStateToFinal(AbstractContinuation.kt:225) at kotlinx.coroutines.experimental.AbstractContinuation.resumeImpl(AbstractContinuation.kt:198) at kotlinx.coroutines.experimental.CancellableContinuationImpl.resumeUndispatched(CancellableContinuation.kt:334) at kotlinx.coroutines.experimental.EventLoopBase$DelayedResumeTask.run(EventLoop.kt:350) at kotlinx.coroutines.experimental.EventLoopBase.processNextEvent(EventLoop.kt:166) at kotlinx.coroutines.experimental.DefaultExecutor.run(DefaultExecutor.kt:61) at java.base/java.lang.Thread.run(Thread.java:844) Caused by: java.lang.ArithmeticException at com.improve_future.kotlincoroutine.MainKt$main$1$job$1$2.invokeSuspend(Main.kt:70) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32) ... 13 more Children are cancelled, but exception is not handled until all children terminate Disconnected from the target VM, address: '127.0.0.1:50981', transport: 'socket' The first child finished its non cancellable block Caught java.lang.ArithmeticException |
次は試しに 例外ハンドラを子のコルーチンに追加して実行してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
fun main(args: Array<String>) = runBlocking { val handler = CoroutineExceptionHandler { _, exception -> println("Caught $exception") } val childHandler = CoroutineExceptionHandler { _, exception -> println(exception.message) exception.printStackTrace() } val job = GlobalScope.launch(handler) { launch(childHandler) { // the first child try { delay(Long.MAX_VALUE) } finally { withContext(NonCancellable) { println("Children are cancelled, but exception is not handled until all children terminate") delay(100) println("The first child finished its non cancellable block") } } } launch(childHandler) { // the second child delay(10) println("Second child throws an exception") throw ArithmeticException() } } job.join() } |
出力は次のようになります。 子のコルーチンに設置した例外ハンドラが機能していないことがわかります。
1 2 3 4 |
Second child throws an exception Children are cancelled, but exception is not handled until all children terminate The first child finished its non cancellable block Caught java.lang.ArithmeticException |
例外の集約
もし、複数の子のコルーチンが例外をスローした場合はどうなるでしょうか。 一般的なルールでは最初の例外がハンドルされます。 しかしそれでは例外を捨ててしまうことになるので、例えばコルーチンが例外を発生させたら、他の例外は消されてしまいます。
この現象に対する対処法として、 それぞれの例外を分けて発生させる方法があります。 しかし、 Deferred.await
は動作の不一致を避けるために同じメカニズムを持っていて、 例外ハンドラにリークするか否かというコルーチン実装の詳細 (子プロセスに委託したか否か) に関係します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
fun main(args: Array<String>) = runBlocking { val handler = CoroutineExceptionHandler { _, exception -> println("Caught $exception with suppressed ${exception.suppressed.contentToString()}") } val job = GlobalScope.launch(handler) { launch { try { delay(Long.MAX_VALUE) } finally { throw ArithmeticException() } } launch { delay(100) throw IOException() } delay(Long.MAX_VALUE) } job.join() } |
このコードは 抑えられた例外をサポートしている JDK7以上 で動作します。
出力は次のようになります。
1 |
Caught java.io.IOException with suppressed [java.lang.ArithmeticException] |
この機構は今のところ Java 1.7以上 で動作します。 JavaScript および Native では近い将来修正されます。
例外のキャンセルは透過的でデフォルトでは透過的で、元の例外が展開されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
fun main(args: Array<String>) = runBlocking { val handler = CoroutineExceptionHandler { _, exception -> println("Caught original $exception") } val job = GlobalScope.launch(handler) { val inner = launch { launch { launch { throw IOException() } } } try { inner.join() } catch (e: CancellationException) { println("Rethrowing CancellationException with original cause") throw e } } job.join() } |
出力は次のようになります。 try
, catch
で捕捉できるのは CancellationException
ですが、 例外ハンドラに渡っているのは キャンセル例外 ではありません。 キャンセルの原因となった IOException
がハンドラに渡っています。
1 2 |
Rethrowing CancellationException with original cause Caught original java.io.IOException |