LoginSignup
5
8

More than 1 year has passed since last update.

Kotlin Coroutine の基本とキャンセルの伝播の話 -launchで伝播する流れを実際に読んでみた-

Last updated at Posted at 2022-03-26
1 / 69

Kotlin Coroutine の特にキャンセルやエラーが伝播していく仕組みについて調べたことのまとめ。
Kotlin coroutineはキャンセルが伝播しますが、あまりイメージがつかめなかったので実際にluanchの内部実装を追って伝播する様子を見てみました。

  • Coroutinesのバージョンはv1.5.2
  • Kotlinのバージョンはv1.5.31

挙動を試すときは、公式のonline Kotlin playground を使用すると便利です。(import文を書けば)coroutineも使用できます。

[2022/4/10 追記]
Kotlin Coroutineに関するまとめの記事の第二弾を公開しました→Kotlin Coroutine のキャンセル周りの話②

[2022/7/4 追記]

  • 内容に誤りがあったため「CoroutineContextとは」 「CoroutineContextの合成」の章を修正しました。
  • ページ内リンクの貼り方が間違っていてクリックしても飛べない箇所があったので修正しました。

Kotlin Coroutine の基本


coroutineとは

軽量スレッドのこと。
みんなでスレッドを共有して、使用者が止まっている時(Thread.sleepにあたるときや別スレッドでの仕事の結果を待っている時)はほかの人が使うようにする。これにより新しくたてるスレッドの数や機会を減らしてリソースの削減を図れます。


coroutineを起動するには

coroutineを起動するにはCoroutineScopeが必要。

【例】

[CoroutineScopeのインスタンス].launch{

}

更に、起動したlaunchasyncのラムダの中もCoroutineScope
なので、launchの中でも直接launchが呼べます。

[CoroutineScopeのインスタンス].launch{
     // this = CoroutineScope
     launch{ // OK!
     }
}

親子関係

起動したlaunchasyncの中のCoroutineScopeは、もとのCoroutineScopeとは別の新しいものですが、親子関係にあります。
=> 子CoroutineScopeと呼ぶことにします。


CoroutineScopeのつくり方

CoroutineContextを指定すると作れます。

image.png

【例】

val scope = CoroutineScope(EmptyCoroutineContext)
scope.launc{
    delay(1000)
}

CoroutineContextとは

[2022/7/4 修正済み]

  • CoroutineScopeの挙動を決めるもの。
  • 1つのCoroutineScopeは必ず1つのCoroutineContextに紐づいています。
  • interfaceです。
  • 内部interfaceとしてElementKey<e:Element>を持ち、ElementCoroutineContextの子interfaceでもあります。
  • 更にElementKeyを内部フィールドとして持ちます。
  • CoroutineContext.Elementは種類ごとに各々ユニークなCoroutineContext.Keyを持っています。
  • CoroutineContext(.Element)同士は加算可能で、同じKeyを持つもの同士を足した場合は最後に足したものが勝ち、違うKeyを持つものを足すと両立します(Mapのような動き)
  • いろんな種類のCoroutineContext.Elementのmapになっています。
  • 実際のCoroutineScopeの挙動は紐づいているCoroutineContext=「CoroutineContext.Elementのmap」の中身により決まります。
  • (JobCoroutineDispatcherなどはCoroutineContext.Elementを実装しています。)

image.png

image.png

CoroutineContext.ElementCoroutineContextを実装しています。


image.png

CoroutineContextの代表的なもの

  • Job : キャンセルや完了を待つときに使います。キャンセルやエラーの伝播も担当します。
  • CoroutineDispatcher : 動くスレッドを決めます。
  • CoroutineName : コルーチンの名前を設定できる。デバッグモードで使えるそうです。自分は使用したことがありません…
  • CoroutineExceptionHandler : CoroutineScopeのなかでtry-catchされなかった例外をハンドリングするためのもの。

CoroutineContextの合成

[2022/7/4 修正済み]

  • 前述の通り、CoroutineContext(.Element)は+演算子で加算できます。
  • Keyの重複を許さない規則のmapである「CoroutineContext.Elementのmap」に別のCoroutineContext(.Element)を加算していくイメージ。
  • +の右要素のKey要素が左要素に含まれていない時→両方保たれる
  • +の右要素のKey要素が左要素に含まれている時 →右要素のものが採用される

【例】

val context1 = Dispatchers.Main + CoroutineName("context1") // mainスレッドで動き、名前は「context1」
val context2 = Dispatchers.Main + Dispatchers.IO            // IOスレッドで動く


キャンセルの伝播の仕組み


前提

coroutineは親CoroutineScopeで起動されたcoroutineがキャンセルされたときに子CoroutineScopeから起動されたcoroutineも自動でキャンセルされたり、子の中で例外が発生したときに同じ親を持つ子が自動でキャンセルされたりする仕組みがあります。(キャンセル・エラーの伝播)


誰が伝播を担うのか?

  • CoroutineContext.ElementのひとつであるJobが担っています。
  • 前述のとおり、各CorouineScopeは必ず1つのCoroutineContextに紐づいています。coroutineが起動したCoroutineScopeに紐づいたCoroutineContextJob同士に親子関係がある時にエラーやキャンセルが伝播します。

伝播の方向

  • Job#cancelが呼ばれたことによるキャンセル(CancellationException発生) : 親→子
  • それ以外のキャッチされなかった例外 : 子→親
    その後受け取った親は自分の子をすべてキャンセルします。(CancellationException発生)

image.png

CoroutineScope#cancel の実装

CoroutineScope#cancelというメソッドがあります。そのCroutineScopeの子や孫などをすべてキャンセルできます。
このメソッドも、内部的にはそのCoroutineScopeに紐づいたCoroutineContextJob要素に対してcancel()を呼んでいます。

image.png

CoroutineScopeの中で例外が発生した時

CoroutineScopeのなかで例外が発生し、キャッチされなかった時は紐づいたCroutineContextのJob要素が例外を受け取っています。

キャンセルの伝播は、
- 今cancel()を呼んだ、もしくはCoroutineScope#cancelを介してcancel()が呼ばれた Jobは誰と親子関係があるのか?
- ここで例外が発生した場合どのJobが受け取って、そのJobは誰と親子関係があるのか?
に注目すると、動きが追えます。


Job要素がないCoroutineContextから作成されたCoroutineScopeは?

val scope = CoroutineScope([CoroutineContextのインスタンス])CoroutineScopeのインスタンスを作成した時、内部的には紐づけたCoroutineContextJob要素を持たない時は新たにJobインスタンスを作成して渡されたCoroutineContextのインスタンスに加算しています。なのでこの場合もCoroutineScopeに紐づいたCoroutineContextJob要素を持ちます。

image.png

CorouitneScope#launchでのキャンセルの伝播の様子を内部実装から追ってみる


初めに、launch内で例外が発生した時の実際のコード出力を見てみる

【コード】

image.png


【実行結果 出力】

1
Exception in thread "DefaultDispatcher-worker-2 @coroutine#3" java.lang.RuntimeException: error
	at FileKt$main$2$1.invokeSuspend(File.kt:22)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
error class kotlinx.coroutines.JobCancellationException
3
  • corouitne 3で発生した例外がcorouitne 1に伝播して、CancellationExceptionが発生しているのが分かると思います。
  • coroutine 1delayしている時にキャンセルの伝播があったので、try-catchでcatchできています。
  • もし、coroutine 1try-catchがなければ、その場でcoroutine 1はキャンセルされてしまい、println("3")は実行されません。

corouitne 3で発生した例外がcorouitne 1に伝播するということはcorouitne 3corouitne 1に親子関係があるということですが、どのように親子関係の構築を実現しているのでしょうか。
「今cancel()を呼んだ、もしくはCoroutineScope#cancelを介してcancel()が呼ばれた Jobは誰と親子関係があるのか?」「ここで例外が発生した場合どのJobが受け取って、そのJobは誰と親子関係があるのか?」に注目して、実際にどのようにキャンセル・エラーが伝播しているのかコードを見ていきたいと思います。


launchの内部実装を見てみる

CoroutineScope.launch

image.png

① コード部分だけ拡大

image.png

1…普段launchに渡しているラムダ関数部。この中で例外が発生した時にどうなるのかを追うのが今回の最終目標です。
2…ラムダ関数部がcoroutine#startに引数として渡されています。
3…直前3行で作成されたインスタンスの入ったcoroutineという変数です。2で使用しています。

coroutineという変数が何なのか見ていきます。


② 引き続きlaunchの実装

image.png

4…何かしらのインスタンスを作成して、変数coroutineに入れています。launchの第2引数startの値によって作成するものは違うようですが、ともにnewContextという値を使用するようです。
5…4で使用しているnewContextを作成しています。newCoroutineContext(CoroutineContext)というメソッドを呼び出しているようです。引数には、launch呼び出し時の第1引数で渡されたCoroutineContextインスタンスを渡しているようです。

newCoroutineContext(CoroutineContext)というメソッドの実装見ていきます。


newCoroutineContextの実装

newCoroutineContext

image.png

CorouitneScopeの拡張メソッドのようです。


newCoroutineContextメソッドの実装を細かく見ていきます。

image.png

1…coroutineContext(interface CoroutineScopeに定義されているCoroutineContext。要するにこのCoroutineScopeに紐づいたCoroutineContextです。)と引数で渡されたcontextを加算して変数combinedに格納しています。contextが右側なので、同じKeyの要素を持つときはcontextの方が優先されます。(順番大事)
2…1で作成したcombineにデバッグモードの時は何かを加算したりして変数debugに格納しています。今回はデバッグモード時については考えないのでdebug==combinedです。
3…combinedDispatchers.DefaultではなくContinuationInterceptor要素を持っていなかったら、debugDispatchers.Defaultを加算して、それをreturn値とする。それ以外の場合はdebugをそのまま返しています。
※ちなみに、CoroutineDispatcherはContinuationInterceptorを実装しています。なので、ContinuationInterceptor要素を持っていなかったら」は「実行スレッドが指定されていなければ…」という意味ととらえてよさそう。(正確には違うと思うけど。)


→以上をまとめると
newCoroutineContextメソッドは「現在のCoroutineScopeが紐づいているCoroutineContextに、引数で渡されたcontextを加算する。さらにそれが実行スレッド要素を持っていれば何もせずそのまま、持っていなければDispatchers.Defaultを加算して、return値とする。」メソッドのようです。


⑤ launchの実装に戻る

「② 引き続きlaunchの実装」で出てきたlaunchの実装に戻ります。

image.png

「④ newCoroutineContextメソッドの実装を細かく見ていきます。」より、5で作成したval newContextはざっくり、「現在のCoroutineScopeが紐づいているCoroutineContextに、引数で渡されたcontextを加算して作成した新しいCoroutineContextインスタンス」であることが分かりました。


次に4で作成しているval coroutineについてみていきます。

image.png

まず、6のstart.isLazytruefalseかということですが、7で設定されているstartのデフォルト値CoroutineStart.DEFAULTtrue falseどちらなのかを見てみたいと思います。
CoroutineStart.DEFAULT
CoroutineStart#isLazy

ということで、デフォルトはstart.isLazy == false のようです。


今回は、start.isLazy == falseの時(8の囲いのところ)だけ見ていこうと思います。
(ちなみにstart.isLazy == trueの時に作成されるLazyStandaloneCoroutineですが、こちらもStandaloneCoroutineを継承しており、キャンセルの伝播という点ではstart.isLazy == false の時と動きに違いはありません。)


StandaloneCoroutine の実装

StandaloneCoroutine

image.png

実装はほぼなく、AbstractCoroutineというものの子クラスのようです。ということで、そちらを見ていきます。


AbstractCoroutine の実装

AbstractCoroutine

image.png

※長すぎて以降スクリーンショットに入りきらず。コード実装の部分を拡大してみます。


image.png

1…CoroutineContextのインスタンス。今回の場合、先ほど作成したval newContextが渡されてくるはずです。
2,3…2がtrueの時、3が実行されます。StandaloneCoroutineでは固定でtrueを設定しているので3は実行されます。3では、1で渡されたparentContext (== newContext)のJob要素をinitParentJobメソッドの引数に渡しています。

したがって次はinitParentJobの実装を見てみます。initParentJobAbstractCoroutineが継承しているJobSupportのメソッドです。


JobSupport#initParentJobの実装

JobSupport の定義

image.png

JobSupportJobの子クラスなことが分かります。


JobSupport#initParentJob

image.png
image.png

引数で渡されたparent (==parentContext ==newContext)のattachChildメソッドを引数として自分自身を渡して実行しています。
したがって次はJob#attachChildメソッドの定義を見てみます。


Job#attachChildメソッドの定義

Job#attachChild

image.png

定義はJobにされているが、実装はJobを実装したクラスが各々行う形のようですが、「渡されたChildJobを自身の子として紐づけ親子関係を作る」メソッドのようです。


例えば、Job()でインスタンス化したJobは、JobImplのインスタンスで、

image.png

JobImplはJobSupportの子クラスで、

image.png

JobSupportでは、attachChildメソッドが実装されています。

image.png

ということでまとめると、Job#attachChildメソッドでは「渡されたChildJobを自身の子として紐づけ親子関係を作って」いるようです。
つまり、AbstractCoroutine、さらにはStandaloneCoroutineは、val newCoroutineを親とするJobでありCoroutineScopeようです。


⑩ ここまでわかったことのまとめ

launchは、自身を起動したCoroutineScopeに紐づいているCoroutineContextと引数で渡されたCoroutineContextを加算し、それがCoroutineDispatcher要素を持たなければさらにDispatchers.Defaultを加算する。
そしてそれを親としたJobでありCoroutineScopeでもあるStandaloneCoroutineクラスのインスタンスcoroutineを作成して、launchの引数で渡されたラムダ関数blockおよびCoroutineStartインスタンスstartを引数にとってstartメソッドを呼ぶ。(coroutine.start(start,coroutine,block))


StandaloneCoroutine#startとは

最後、StandaloneCoroutine#startとは何をしていて、引数で渡されたラムダ関数の中で例外が発生したときにどのJobが受け取って、そのJobは誰と親子関係があるのか?を見ていきます。
まず、StandaloneCoroutine#startAbstractCoroutine#startです。したがって、そちらの実装を見てみます。

AbstractCoroutine#start

image.png

CoroutineStart#invokeが呼ばれているようです。


CoroutineStart#invoke

image.png

第2引数のreceiverをレシーバーにして第1引数のblock(ラムダ関数)を実行するようです。
つまり、「blockで例外が発生したときに、そのエラーはreceiverが受ける」と考えてよさそうです。


おまけ

細かく見るのはここで力尽きてしまいましたが、以下少しだけさらに深いところまでコードを追っていきます。


launchのデフォルトは、DEFAULTになるので、赤枠の部分が実行されるはずです。赤枠の部分=(suspend (R) -> T)#startCoroutineCancellableの実装を見てみます。

(suspend (R) -> T)#startCoroutineCancellable

image.png

この後は

runSafety

image.png

(suspend R.() -> T)#createCoroutineUnintercepted

image.png

のように続いていきます。


⑫まとめ

さて、⑩⑪の内容をまとめると、

launchは、自身を起動したCoroutineScopeに紐づいているCoroutineContextと引数で渡されたCoroutineContextを加算し、それがCoroutineDispatcher要素を持たなければさらにDispatchers.Defaultを加算する。

・そしてそれを親としたJobでありCoroutineScopeでもあるStandaloneCoroutineクラスのインスタンスcoroutineを作成して、launchの引数で渡されたラムダ関数blockおよびCoroutineStartインスタンスstartを引数にとってstartメソッドを呼ぶ。(coroutine.start(start,coroutine,block))

coroutine.start(start,coroutine,block)では、blockで例外が発生した場合は第2引数のcoroutineがそのエラーを受け取る。

となります。


つまりlaunchで起動したcoroutineで例外が発生した場合

launch呼び出し時に渡されたCoroutineContextJob要素またはlaunchを起動したCoroutineScopeに紐づいたCoroutineContextJob

を親にしたCoroutineScopeかつJobであるStandaloneCoroutine

例外を受け取ります。


CoroutineScopeの中で例外が発生した時 で提示した、キャンセルの伝播を追う時の着目点

・ 今cancel()を呼んだ、もしくはCoroutineScope#cancelを介してcancel()が呼ばれた Jobは誰と親子関係があるのか?
・ ここで例外が発生した場合どのJobが受け取って、そのJobは誰と親子関係があるのか?
= launch(に渡したラムダ関数)のなかで例外が発生した場合
・いったいどのJobが例外を受けとる?
・そのJobは誰と親子関係がある?


・いったいどのJobが例外を受けとる? → lauchの中で作ったStandaloneCoroutine
・そのJobは誰と親子関係がある? → launch呼び出し時に渡されたCoroutineContextJob要素またはlaunchを起動したCoroutineScopeに紐づいたCoroutineContextJobを親にしている。


以上、これでlaunchに渡されたラムダの中で例外が発生したときに、エラーが伝播するために親子関係のあるJobが作成されラムダ関数が実行される流れを具体的に追えました。


コード例の場合のキャンセルの伝播の様子

初めにlaunch内で例外が発生した時の実際のコード出力を見てみる で出したコード例

image.png


coroutine 3で例外が発生
coroutine 3で発生した例外を受け取るのは、【coroutine 3launchを起動したCoroutineScopeに紐づいたCoroutineContextJob要素】=【coroutine 2に紐づいたCoroutineContextJob要素】を親にしているStandaloneCoroutine
→親である【launchを起動したCoroutineScopeに紐づいたCoroutineContextJob要素】=【coroutine 2に紐づいたCoroutineContextJob要素】にエラーが伝播
→受け取った【coroutine 2に紐づいたCoroutineContextJob要素】は、【coroutine 2launchを起動したCoroutineScopeに紐づいたCoroutineContextJob要素】=【scopeに紐づいたCoroutineContextJob要素】を親にしているStandaloneCoroutine
→【scopeに紐づいたCoroutineContextJob要素】にエラーが伝播
→もうひとつの【scopeに紐づいたCoroutineContextJob要素】を親にしているStandaloneCoroutineに紐づいているCoroutineScopeであるcoroutine 1にキャンセルが伝播
→それがdelay (100L)実行中に起きるので、そこで例外が発生しcatch節で捕捉され、println("2")は実行されずprintln("1")が実行される


※ちなみに、実行時のコンソールログの

Exception in thread "DefaultDispatcher-worker-2 @coroutine#3" java.lang.RuntimeException: error
	at FileKt$main$2$1.invokeSuspend(File.kt:22)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)

部分は、coroutine 3CoroutineContext自体にエラーが捕捉されずに到達してしまったので、バックスレッド(coroutine 3CoroutineContextに紐づいているDispatchers.Defaultスレッド)で例外が発生している旨のログが出ているのだと思われます。


launchに渡したラムダの中で例外が発生したときにキャンセルの伝播を防ぐためには

では、このコード例でエラーの伝播をさせずにcoroutine 1を正常系だけ通すにはどうしたらよいでしょうか?


⑫まとめの内容の

launch(に渡したラムダ関数)のなかで例外が発生した場合
・いったいどのJobが例外を受けとる? → lauchの中で作ったStandaloneCoroutine
・そのJob派と誰と親子関係がある? → launch呼び出し時に渡されたCoroutineContextJob要素またはlaunchを起動したCoroutineScopeに紐づいたCoroutineContextJobを親にしている。

を念頭に置くと以下4つの方法があると思います。


方法①

coroutine 3で例外をthrowする部分をtry-catchで囲む

image.png


【実行結果 出力】

1
2
3

方法②

coroutine 3を起動するlaunchに新しいJob(を含んだCoroutineContext)インスタンスを渡す

image.png


【実行結果 出力】

1
Exception in thread "DefaultDispatcher-worker-1 @coroutine#3" java.lang.RuntimeException: error
	at FileKt$main$2$1.invokeSuspend(File.kt:22)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
2
3

※この場合、coroutine 3CoroutineContext自体にエラーが捕捉されずに到達してしまうのは変わらないのでバックスレッド(coroutine 3CoroutineContextに紐づいているDispatchers.Defaultスレッド)で例外が発生している旨のログは出ますが、キャンセルは伝播しないのでprintln("1") println("2") println("3")はすべて実行されています。


方法③

coroutine 2を起動するlaunchに新しいJob(を含んだCoroutineContext)インスタンスを渡す

image.png


【実行結果 出力】

1
Exception in thread "DefaultDispatcher-worker-1 @coroutine#3" java.lang.RuntimeException: error
	at FileKt$main$2$1.invokeSuspend(File.kt:22)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
2
3

※この場合も、coroutine 3CoroutineContext自体にエラーが捕捉されずに到達してしまうのは変わらないのでバックスレッド(coroutine 3CoroutineContextに紐づいているDispatchers.Defaultスレッド)で例外が発生している旨のログは出ますが、キャンセルは伝播しないのでprintln("1") println("2") println("3")はすべて実行されています。


方法④

coroutine 1を起動するlaunchに新しいJob(を含んだCoroutineContext)インスタンスを渡す

image.png


【実行結果 出力】

1
Exception in thread "DefaultDispatcher-worker-2 @coroutine#3" java.lang.RuntimeException: error
	at FileKt$main$2$1.invokeSuspend(File.kt:22)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
2
3

※この場合も、coroutine 3CoroutineContext自体にエラーが捕捉されずに到達してしまうのは変わらないのでバックスレッド(coroutine 3CoroutineContextに紐づいているDispatchers.Defaultスレッド)で例外が発生している旨のログは出ますが、キャンセルは伝播しないのでprintln("1") println("2") println("3")はすべて実行されています。


参考資料

https://kotlinlang.org/docs/coroutines-basics.html#scope-builder-and-concurrency
https://zenn.dev/at_sushi_at/books/edf63219adfc31
https://mahata.gitlab.io/post/2019-04-23-coroutines-kotlin/
https://speakerdeck.com/ntaro/kotlin-contracts-number-m3kt?slide=12

その他、Kotlin / Kotlin Coroutine の公式サイトなど

5
8
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
8