Kotlin 1.3: コルーチンでの例外の扱い方 (2/2)


Kotlin 1.3: コルーチンでの例外の扱い方 (1/2) からの続きです。

キャンセルと例外

キャンセルは例外と強い関係があります。 コルーチンはキャンセルの際に、 内部的に CoroutineException を使っています。 この例外は全ての例外ハンドラに無視されるので、 catch ブロック で利用可能な 付加的なデバッグ情報のためにしか使われません。 コルーチンが 原因なく Job.cancel によってキャンセルされる時、 そのコルーチンは消滅しますが、 親のコルーチンは消滅しません。 例外の原因を発生させずにキャンセルする機構は、 親のコルーチンが、 自分自身をキャンセルすることなく子のコルーチンを取り消すための仕組みです。

このコードの出力は次のようになります。

Kotlin のドキュメントでは、 "Child is cancelled" も出力されるよう書かれていますが、 実際に試したところでは出力されませんでした。

コルーチンが CancellationException 以外の例外を検出した場合、 その例外によって親のコルーチンを取り消します。 この動作はオーバーライドすることはできず、 CoroutineExceptionHandler の実装に依存しない、 構造化された並行性のための安定したコルーチン階層を提供するために使用されます。 元の例外は、すべての子コルーチンが終了したときに親のコルーチンによって処理されます。

次の例で CoroutineExceptionHandler を常に GlobalScope で作成されたコルーチンにインストールしている理由は次の通りです。 メインのコルーチンは、インストールされたハンドラに関係なく、子コルーチンが例外で完了したときに常にキャンセルされるためです。 メインの runBlocking のスコープで起動されるコルーチンに例外ハンドラを設置してもキャンセルを止めることはできません。

このコードの出力は次のようになります。

親のコルーチンで、子のコルーチンを2つ起動しています。 子の一方で例外が発生した時、もう一方の子が terminate されます。 そして、 finally の処理が終わると ようやく親のコルーチンに例外が伝播します。

例外が発生していない方の子コルーチンでは、どんな例外が出ているのでしょうか。 catch ブロック を記述して内容を見てみます。

出力は次のようになります。 JobCancellationException が出ていることがわかります。

次は試しに 例外ハンドラを子のコルーチンに追加して実行してみます。

出力は次のようになります。 子のコルーチンに設置した例外ハンドラが機能していないことがわかります。

例外の集約

もし、複数の子のコルーチンが例外をスローした場合はどうなるでしょうか。 一般的なルールでは最初の例外がハンドルされます。 しかしそれでは例外を捨ててしまうことになるので、例えばコルーチンが例外を発生させたら、他の例外は消されてしまいます。

この現象に対する対処法として、 それぞれの例外を分けて発生させる方法があります。 しかし、 Deferred.await は動作の不一致を避けるために同じメカニズムを持っていて、 例外ハンドラにリークするか否かというコルーチン実装の詳細 (子プロセスに委託したか否か) に関係します。

このコードは 抑えられた例外をサポートしている JDK7以上 で動作します。

出力は次のようになります。

この機構は今のところ Java 1.7以上 で動作します。 JavaScript および Native では近い将来修正されます。

例外のキャンセルは透過的でデフォルトでは透過的で、元の例外が展開されます。

出力は次のようになります。 try, catch で捕捉できるのは CancellationException ですが、 例外ハンドラに渡っているのは キャンセル例外 ではありません。 キャンセルの原因となった IOException がハンドラに渡っています。