目次
Kotlin 1.3 から使えるようになる Coroutine のドキュメントを読み、 まとめました。
キャンセルはコルーチンの階層全体において双方向の伝播関係にあります。 しかし、方向の指定されていないキャンセルが必要になった場合はどうしましょうか。
その良い例が 定義されたジョブを持つ UI コンポーネント です。 もし UI の子タスクが失敗した場合、 UI全体において常にキャンセル(効果的な停止)が必要なわけではありません。 UIコンポーネントが削除された場合、そしてそのジョブがキャンセルされている場合、全ての子のジョブは結果が必要なくなったため、失敗する必要があります。
他の例では、いくつかの子のジョブを生成し、その実行を監督し、失敗を追跡し、失敗した子のジョブを再起動するサーバープロセスです。
監督ジョブ
これらの目的のためには SupervisionJob
が使えます。 これは例外が下方にのみ伝播される通常のジョブに似ています。 例を見てみます。
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 |
fun main(args: Array<String>) = runBlocking { val supervisor = SupervisorJob() with(CoroutineScope(coroutineContext + supervisor)) { // launch the first child -- its exception is ignored for this example (don't do this in practise!) val firstChild = launch(CoroutineExceptionHandler { _, _ -> }) { println("First child is failing") throw AssertionError("First child is cancelled") } // launch the second child val secondChild = launch { firstChild.join() // Cancellation of the first child is not propagated to the second child println("First child is cancelled: ${firstChild.isCancelled}, but second one is still active") try { delay(Long.MAX_VALUE) } finally { // But cancellation of the supervisor is propagated println("Second child is cancelled because supervisor is cancelled") } } // wait until the first child fails & completes firstChild.join() println("Cancelling supervisor") supervisor.cancel() secondChild.join() } } |
出力は次のようになります。
1 2 |
First child is failing Cancelling supervisor |
公式ドキュメントとは異なる結果となりました。 最初の子ジョブがキャンセルされており、ふたつめの子ジョブが実行されていないので、コードは意図した挙動になっています。
delay
を使ってもう少しゆっくりと眺めてみます。
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 |
fun main(args: Array<String>) = runBlocking { val supervisor = SupervisorJob() with(CoroutineScope(coroutineContext + supervisor)) { // launch the first child -- its exception is ignored for this example (don't do this in practise!) val firstChild = launch(CoroutineExceptionHandler { _, _ -> }) { println("First child is failing") throw AssertionError("First child is cancelled") } // launch the second child val secondChild = launch { firstChild.join() // Cancellation of the first child is not propagated to the second child println("First child is cancelled: ${firstChild.isCancelled}, but second one is still active") try { delay(Long.MAX_VALUE) } finally { // But cancellation of the supervisor is propagated println("Second child is cancelled because supervisor is cancelled") } } // wait until the first child fails & completes delay(1000L) firstChild.join() delay(1000L) println("Cancelling supervisor") delay(1000L) supervisor.cancel() delay(1000L) secondChild.join() } } |
このようにすると、出力は公式ドキュメントと同じになりました。 ふたつめの子ジョブを実行するスキを与えてやったことになります。
1 2 3 4 |
First child is failing First child is cancelled: true, but second one is still active Cancelling supervisor Second child is cancelled because supervisor is cancelled |
監督スコープ
scoped
の同時実行性
スコープ付き同時実行には coroutineScope
の代わりに supervisorScope
が利用可能です。 supervisorScope
は一方向のみにキャンセルを伝播し、自身が失敗した場合にのみすべての子を取り消します。 これは、 coroutineScope
と同様に、 完了前にすべての子の完了を待ちます。
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 { try { supervisorScope { val child = launch { try { println("Child is sleeping") delay(Long.MAX_VALUE) } finally { println("Child is cancelled") } } // Give our child a chance to execute and print using yield yield() println("Throwing exception from scope") throw AssertionError() } } catch(e: AssertionError) { println("Caught assertion error") } } |
この公式ドキュメントのコードを実行すると次のようになりました。
1 2 |
Throwing exception from scope Caught assertion error |
公式ドキュメントとは結果が異なりました。 上と同じように、 yield
の前後に delay(500L)
を挿入すると、 公式ドキュメントに書かれている結果と同じになりました。
1 2 3 4 |
Child is sleeping Throwing exception from scope Child is cancelled Caught assertion error |
監督コルーチンでの例外
他に、一般のジョブと監督ジョブ(スーパバイザジョブ)との大きな違いとして例外処理があります。 スーパバイザジョブを使う場合、 子の例外が親に伝播されないため、 全ての子は例外を子自身が例外処理のメカニズムを使って処理する必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
fun main(args: Array<String>) = runBlocking { val handler = CoroutineExceptionHandler { _, exception -> println("Caught $exception") } supervisorScope { val child = launch(handler) { println("Child throws an exception") throw AssertionError() } println("Scope is completing") } println("Scope is completed") } |
出力は次のようになります。
1 2 3 4 |
Scope is completing Child throws an exception Caught java.lang.AssertionError Scope is completed |
もちろん、子のジョブから例外ハンドラを取り去ると、エラーがそのまま出力されます。
1 2 3 4 5 6 7 8 9 10 |
fun main(args: Array<String>) = runBlocking { supervisorScope { val child = launch { println("Child throws an exception") throw AssertionError() } println("Scope is completing") } println("Scope is completed") } |
出力は次のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
Scope is completing Child throws an exception Exception in thread "main @coroutine#2" java.lang.AssertionError at com.improve_future.kotlincoroutine.MainKt$main$1$1$child$1.invokeSuspend(Main.kt:48) at com.improve_future.kotlincoroutine.MainKt$main$1$1$child$1.invoke(Main.kt) at kotlin.coroutines.experimental.migration.ExperimentalSuspendFunction1Migration.invoke(CoroutinesMigration.kt:134) at kotlin.coroutines.experimental.migration.ExperimentalSuspendFunction1Migration.invoke(CoroutinesMigration.kt:130) at kotlin.coroutines.experimental.intrinsics.IntrinsicsKt__IntrinsicsJvmKt$createCoroutineUnchecked$$inlined$buildContinuationByInvokeCall$IntrinsicsKt__IntrinsicsJvmKt$2.resume(IntrinsicsJvm.kt:122) at kotlin.coroutines.experimental.intrinsics.IntrinsicsKt__IntrinsicsJvmKt$createCoroutineUnchecked$$inlined$buildContinuationByInvokeCall$IntrinsicsKt__IntrinsicsJvmKt$2.resume(IntrinsicsJvm.kt:98) at kotlinx.coroutines.experimental.DispatchedTask$DefaultImpls.run(Dispatched.kt:168) at kotlinx.coroutines.experimental.DispatchedContinuation.run(Dispatched.kt:13) at kotlinx.coroutines.experimental.EventLoopBase.processNextEvent(EventLoop.kt:166) at kotlinx.coroutines.experimental.BlockingCoroutine.joinBlocking(Builders.kt:69) at kotlinx.coroutines.experimental.BuildersKt__BuildersKt.runBlocking(Builders.kt:45) at kotlinx.coroutines.experimental.BuildersKt.runBlocking(Unknown Source) at kotlinx.coroutines.experimental.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:35) at kotlinx.coroutines.experimental.BuildersKt.runBlocking$default(Unknown Source) at com.improve_future.kotlincoroutine.MainKt.main(Main.kt:41) Scope is completed |
supervisorScope
は、 引数を1つしか取らないので、 例外ハンドラなどは渡せないようになっています。
1 2 3 4 5 |
public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R = suspendCoroutineUninterceptedOrReturn { uCont -> val coroutine = SupervisorCoroutine(uCont.context, uCont) coroutine.startUndispatchedOrReturn(coroutine, block) } |