Kotlin 1.3: コルーチンのキャンセルとタイムアウト


コルーチンのキャンセルとタイムアウトについて、ドキュメントを読みました。

検証環境

  • OpenJDK 10.0.2
  • Kotlin 1.3 M2

コルーチンの実行をキャンセルする

長時間実行するアプリケーションでは、時にバックグラウンドのコルーチンを適当な粒度でコントロールする必要が出てきます。 たとえば、ユーザがコルーチン実行中のページを閉じてしまい、もうその結果がいらなくなった場合。 launch 関数 は Job を返却します。 その Job を使って、実行中のコルーチンをキャンセルできます。

この例では途中で Job をキャンセルしています。 次のような出力になります。

上の例では cancel, join を 2行に渡って書いていますが、 cancelAndJoin というメソッドひとつで同等のことができます。

キャンセルは協力的

コルーチンのコードは協調して取り消し可能でなければなりません。 キャンセルするには外部からキャンセルの指示を出すだけでなく、 コルーチンそのものがキャンセル可能になっている必要があります。 kotlinx.coroutinesのサスペンディングファンクションはすべてキャンセル可能です。 サスペンディングファンクションは、コルーチンのキャンセルをチェックし、キャンセルすると CancellationException をスローします。 しかし、コルーチンが計算実行中で、取り消しをチェックしていない場合は、次の例のように取り消すことはできません。

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

計算するコードをキャンセル可能にする

計算コードをキャンセル可能にするには2つのアプローチがあります。 ひとつは定期的にキャンセル可能であることをチェックするためのセスペンディングファンクションを実行することです。 そのために利用可能な yield という関数があります。 もうひとつは明示的にキャンセル状態をチェックすることです。 ここでは後者の例を示します。

先のコードで while (i < 5) の部分を while (isActive) に変更してみます。

実行結果は次のようになります。

isActive は コルーチンのキャンセル状態をチェックできる CoroutineScope の拡張プロパティです。

finally でリソースをクローズする

キャンセル可能なサスペンディングファンクションはキャンセル時に 通常の方法で処理可能な CancellationException をスローします。 たとえば try {...} finally {...} 式 と Kotlin の use 関数 は コルーチンがキャンセルされた場合に 最終処理を通常通り実行します。

この出力は次の通りです。

キャンセル実行後に、 finally のブロックが実行されていることがわかります。

キャンセルできないブロックの実行

サスペンディングファンクションを 先の例の finally のブロックで実行しようとすると、 このコードを実行しているコルーチンがキャンセルされるため、CancellationExceptionが発生します。 通常、ファイルを閉じる、ジョブをキャンセルする、またはあらゆる種類の通信チャネルを閉じるなど、正常に動作するすべてのクローズ操作は非ブロッキングであり、 サスペンディングファンクションを含まないため、これは問題ではありません。 ただし、キャンセルされたコルーチンで中断する必要がある特殊なケースでは、withContext 関数 と NonCancellable コンテキスト を使用して、 対応するコードを withContext(NonCancellable){...} でラップすることができます。

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

finally のブロックのコードが完全に実行されているのがわかります。 比較のため、 withContext を使わないコードを実行してみます。

サスペンディングファンクション delay 以降は出力されません。

タイムアウト

コルーチンをキャンセルする最も自明な理由は実行時間があるタイムアウト閾値を超過することです。 対応するジョブへの参照を手動で追跡し、 停止の後にキャンセルするための別のコルーチンを起動することもできますが、 withTimeout 関数 を使うと同様のことができます。

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

withTimeout 関数 からは TimeoutCancellationException がスローされます。 これは CancellationException のサブクラスです。

これまでの出力結果との違いとして、 スタックトレースが出力されています。 以前のコードではスタックトレースが出力されていませんでした。 キャンセルされたコルーチンの内部では、 CancellationException はコルーチン完了の通常の理由と見なされるのですが、 この例では main 関数 の 直下で TimeoutCancellationException をスローしています。

キャンセルは例外(Exception)なので、全てのリソースが通常の方法でクローズされます。 もしタイムアウトに特別な処理を追加する場合は、 withTimeout ごと try {...} catch (e: TimeoutCancellationException) {...} で囲んでしまうか、 withTimeoutOrNull 関数 を使用するといいです。 withTimeout 関数 は、 withTimeout に似ていますが、 タイムアウト時に例外をスローするのではなく、 null を返します。

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

タイムアウトしていますが、エラーをスローせず、 null を返しています。 最終行の println も実行されています。