目次
コルーチンのキャンセルとタイムアウトについて、ドキュメントを読みました。
検証環境
- OpenJDK 10.0.2
- Kotlin 1.3 M2
コルーチンの実行をキャンセルする
長時間実行するアプリケーションでは、時にバックグラウンドのコルーチンを適当な粒度でコントロールする必要が出てきます。 たとえば、ユーザがコルーチン実行中のページを閉じてしまい、もうその結果がいらなくなった場合。 launch
関数 は Job
を返却します。 その Job
を使って、実行中のコルーチンをキャンセルできます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
fun main(args: Array<String>) = runBlocking { val job = launch { repeat(1000) { i -> println("I'm sleeping $i ...") delay(500L) } } delay(1300L) // delay a bit println("main: I'm tired of waiting!") job.cancel() // cancels the job job.join() // waits for job's completion println("main: Now I can quit.") } |
この例では途中で Job をキャンセルしています。 次のような出力になります。
1 2 3 4 5 |
I'm sleeping 0 ... I'm sleeping 1 ... I'm sleeping 2 ... main: I'm tired of waiting! main: Now I can quit. |
上の例では cancel
, join
を 2行に渡って書いていますが、 cancelAndJoin
というメソッドひとつで同等のことができます。
キャンセルは協力的
コルーチンのコードは協調して取り消し可能でなければなりません。 キャンセルするには外部からキャンセルの指示を出すだけでなく、 コルーチンそのものがキャンセル可能になっている必要があります。 kotlinx.coroutines
のサスペンディングファンクションはすべてキャンセル可能です。 サスペンディングファンクションは、コルーチンのキャンセルをチェックし、キャンセルすると CancellationException
をスローします。 しかし、コルーチンが計算実行中で、取り消しをチェックしていない場合は、次の例のように取り消すことはできません。
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 startTime = System.currentTimeMillis() val job = launch(Dispatchers.Default) { var nextPrintTime = startTime var i = 0 while (i < 5) { // computation loop, just wastes CPU // print a message twice a second if (System.currentTimeMillis() >= nextPrintTime) { println("I'm sleeping ${i++} ...") nextPrintTime += 500L } } } delay(1300L) // delay a bit println("main: I'm tired of waiting!") job.cancelAndJoin() // cancels the job and waits for its completion println("main: Now I can quit.") } |
このコードの出力は次のようになります。
1 2 3 4 5 6 7 |
I'm sleeping 0 ... I'm sleeping 1 ... I'm sleeping 2 ... main: I'm tired of waiting! I'm sleeping 3 ... I'm sleeping 4 ... main: Now I can quit. |
計算するコードをキャンセル可能にする
計算コードをキャンセル可能にするには2つのアプローチがあります。 ひとつは定期的にキャンセル可能であることをチェックするためのセスペンディングファンクションを実行することです。 そのために利用可能な yield
という関数があります。 もうひとつは明示的にキャンセル状態をチェックすることです。 ここでは後者の例を示します。
先のコードで while (i < 5)
の部分を while (isActive)
に変更してみます。
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 startTime = System.currentTimeMillis() val job = launch(Dispatchers.Default) { var nextPrintTime = startTime var i = 0 while (isActive) { // cancellable computation loop // print a message twice a second if (System.currentTimeMillis() >= nextPrintTime) { println("I'm sleeping ${i++} ...") nextPrintTime += 500L } } } delay(1300L) // delay a bit println("main: I'm tired of waiting!") job.cancelAndJoin() // cancels the job and waits for its completion println("main: Now I can quit.") } |
実行結果は次のようになります。
1 2 3 4 5 |
I'm sleeping 0 ... I'm sleeping 1 ... I'm sleeping 2 ... main: I'm tired of waiting! main: Now I can quit. |
isActive
は コルーチンのキャンセル状態をチェックできる CoroutineScope
の拡張プロパティです。
finally でリソースをクローズする
キャンセル可能なサスペンディングファンクションはキャンセル時に 通常の方法で処理可能な CancellationException
をスローします。 たとえば try {...} finally {...}
式 と Kotlin の use
関数 は コルーチンがキャンセルされた場合に 最終処理を通常通り実行します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
fun main(args: Array<String>) = runBlocking { val job = launch { try { repeat(1000) { i -> println("I'm sleeping $i ...") delay(500L) } } finally { println("I'm running finally") } } delay(1300L) // delay a bit println("main: I'm tired of waiting!") job.cancelAndJoin() // cancels the job and waits for its completion println("main: Now I can quit.") } |
この出力は次の通りです。
1 2 3 4 5 6 |
I'm sleeping 0 ... I'm sleeping 1 ... I'm sleeping 2 ... main: I'm tired of waiting! I'm running finally main: Now I can quit. |
キャンセル実行後に、 finally のブロックが実行されていることがわかります。
キャンセルできないブロックの実行
サスペンディングファンクションを 先の例の finally
のブロックで実行しようとすると、 このコードを実行しているコルーチンがキャンセルされるため、CancellationException
が発生します。 通常、ファイルを閉じる、ジョブをキャンセルする、またはあらゆる種類の通信チャネルを閉じるなど、正常に動作するすべてのクローズ操作は非ブロッキングであり、 サスペンディングファンクションを含まないため、これは問題ではありません。 ただし、キャンセルされたコルーチンで中断する必要がある特殊なケースでは、withContext
関数 と NonCancellable
コンテキスト を使用して、 対応するコードを withContext(NonCancellable){...}
でラップすることができます。
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 job = launch { try { repeat(1000) { i -> println("I'm sleeping $i ...") delay(500L) } } finally { withContext(NonCancellable) { println("I'm running finally") delay(1000L) println("And I've just delayed for 1 sec because I'm non-cancellable") } } } delay(1300L) // delay a bit println("main: I'm tired of waiting!") job.cancelAndJoin() // cancels the job and waits for its completion println("main: Now I can quit.") } |
この出力は次のようになります。
1 2 3 4 5 6 7 |
I'm sleeping 0 ... I'm sleeping 1 ... I'm sleeping 2 ... main: I'm tired of waiting! I'm running finally And I've just delayed for 1 sec because I'm non-cancellable main: Now I can quit. |
finally
のブロックのコードが完全に実行されているのがわかります。 比較のため、 withContext
を使わないコードを実行してみます。
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 { try { repeat(1000) { i -> println("I'm sleeping $i ...") delay(500L) } } finally { println("I'm running finally") delay(1000L) println("And I've just delayed for 1 sec because I'm non-cancellable") } } delay(1300L) // delay a bit println("main: I'm tired of waiting!") job.cancelAndJoin() // cancels the job and waits for its completion println("main: Now I can quit.") } |
1 2 3 4 5 6 7 8 9 10 |
I'm sleeping 0 ... I'm sleeping 1 ... I'm sleeping 2 ... Exception in thread "main" kotlinx.coroutines.experimental.TimeoutCancellationException: Timed out waiting for 1300 ms at kotlinx.coroutines.experimental.TimeoutKt.TimeoutCancellationException(Timeout.kt:127) at kotlinx.coroutines.experimental.TimeoutCoroutine.run(Timeout.kt:91) at kotlinx.coroutines.experimental.EventLoopBase$DelayedRunnableTask.run(EventLoop.kt:359) 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) |
サスペンディングファンクション delay
以降は出力されません。
タイムアウト
コルーチンをキャンセルする最も自明な理由は実行時間があるタイムアウト閾値を超過することです。 対応するジョブへの参照を手動で追跡し、 停止の後にキャンセルするための別のコルーチンを起動することもできますが、 withTimeout
関数 を使うと同様のことができます。
1 2 3 4 5 6 7 8 |
fun main(args: Array<String>) = runBlocking { withTimeout(1300L) { repeat(1000) { i -> println("I'm sleeping $i ...") delay(500L) } } } |
出力は次のようになります。
1 2 3 4 |
I'm sleeping 0 ... I'm sleeping 1 ... I'm sleeping 2 ... Exception in thread "main" kotlinx.coroutines.experimental.TimeoutCancellationException: Timed out waiting for 1300 ms |
withTimeout
関数 からは TimeoutCancellationException
がスローされます。 これは CancellationException
のサブクラスです。
これまでの出力結果との違いとして、 スタックトレースが出力されています。 以前のコードではスタックトレースが出力されていませんでした。 キャンセルされたコルーチンの内部では、 CancellationException
はコルーチン完了の通常の理由と見なされるのですが、 この例では main
関数 の 直下で TimeoutCancellationException
をスローしています。
キャンセルは例外(Exception
)なので、全てのリソースが通常の方法でクローズされます。 もしタイムアウトに特別な処理を追加する場合は、 withTimeout
ごと try {...} catch (e: TimeoutCancellationException) {...}
で囲んでしまうか、 withTimeoutOrNull
関数 を使用するといいです。 withTimeout
関数 は、 withTimeout
に似ていますが、 タイムアウト時に例外をスローするのではなく、 null
を返します。
1 2 3 4 5 6 7 8 9 10 |
fun main(args: Array<String>) = runBlocking { val result = withTimeoutOrNull(1300L) { repeat(1000) { i -> println("I'm sleeping $i ...") delay(500L) } "Done" // will get cancelled before it produces this result } println("Result is $result") } |
このコードの出力は次のようになります。
1 2 3 4 |
I'm sleeping 0 ... I'm sleeping 1 ... I'm sleeping 2 ... Result is null |
タイムアウトしていますが、エラーをスローせず、 null
を返しています。 最終行の println
も実行されています。