Kotlin Coroutine について勉強したことのまとめ第二弾。
第一弾はこちら
Kotlin Coroutine の基本とキャンセルの伝播の話 -launchで伝播する流れを実際に読んでみた-
環境/前提
- Coroutinesのバージョンはv1.5.2
- Kotlinのバージョンはv1.5.31
挙動を試すときは、公式のonline Kotlin playground を使用すると便利です。(import
文を書けば)coroutineも使用できます。
async
とwithContext
のキャンセルの伝播の仕様
第一弾でlaunchのコードを追ったときのように、async
とwithContext
も軽く内部実装やコメントを見つつメソッドの仕様やキャンセルの伝播の仕様を紹介します。
aync
launchを追ったときの教訓よりデフォルトはstart.isLazy
はfalse
です。
したがって、DeferredCoroutine
の実装を見てみます。
DeferredCoroutine
はlaunchを追ったときにも出てきたAbstractCoroutine
の子クラスのようです。
また、その他async
の内部実装に使われているnewCoroutineContext
メソッドやAbstaractCoroutine#start
を呼ぶ構造もlaunch
と一緒です。
よって、launchを追ったときの結果から、
async
に引数として渡されたラムダ関数の中で例外が発生した場合は,
async
呼び出し時に渡されたCoroutineContext
のJob
要素またはasync
を起動したCoroutineScope
に紐づいたCoroutineContext
のJob
を親にしたCoroutineScope
かつJob
であるDeferredCoroutine
が例外を受け取ります。
※ちなみに、start.isLazy
がtrue
の時に使われているLazyDeferredCoroutine
は、DeferredCoroutine
の子クラスです。
async
のコード例と実行結果です。
【例】
【実行結果 出力】
2
1
catch: java.lang.Exception: error
Exception in thread "DefaultDispatcher-worker-2 @coroutine#1" java.lang.Exception: error
at FileKt$main$1$1$1.invokeSuspend(File.kt:14)
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 3
で発生した例外はcorouitne 3
に紐づいた'Job'であるDeferredCoroutine
が受け取り、Job
の親子関係に沿ってcorouitne 4
のJob
まで伝わりキャンセルされるので、println("3")
は実行されません。
ちなみに、async
に渡したラムダの中で発生した例外はasync#await()
をtry-catch
で囲むと捕捉できます(先ほどのコード例println(catch: $e)
で実行結果のcatch: java.lang.Exception: error
が出力される部分)が、それでもcoroutine
のキャンセルの伝播が止められるわけではありません。
それは、エラーの伝播は「そのコルーチンに紐づいたJob
へ例外が到達したか」に依存するのに対して、async
の中で例外が発生した場合にasync#await()
はその例外をthrow
する仕様でasync#await()
を囲んだtry-catch
でキャッチできているのはそのawait
が投げた例外だからです。
なので、以下のようにawait
を呼ばないとcatch
節は呼ばれないし、
【例】
【実行結果 出力】
2
1
Exception in thread "DefaultDispatcher-worker-1 @coroutine#4" java.lang.Exception: error
at FileKt$main$1$1$1.invokeSuspend(File.kt:14)
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)
離れたところでawait
を呼びそれをtry-catch
で囲んでもcatch
節で例外を捕捉できます。そしてキャンセルの伝播自体は起きます。
【例】
【実行結果 出力】
2
1
catch: java.lang.Exception: error
Exception in thread "DefaultDispatcher-worker-2 @coroutine#1" java.lang.Exception: error
at FileKt$main$1$async1$1$1.invokeSuspend(File.kt:14)
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)
withContext
※長いのでスクリーンショットがコメント部と実装部の2枚になりました。
① … 現在のCoroutineContext
に、引数で渡されたCoroutineContext
のインスタンスを加算しています。(順番大事)
② … ScopeCoroutine
こちらもAbstractCoroutine
の子クラスのようです。
ただし、launch
やasync
との違いとして、赤く囲われた部分が重大なポイントになっています。
final override val isScopedCoroutine : Boolean get() = true とは?
override
している元はどこ?
→ JobSupport
で定義されています。
※AbstractCoroutine
はJobSupport
を継承しています。
→ isScopedCoroutine
がtrue
になるときは、例外をそれより内側のスコープにしか投げない、scoped coroutine
と呼ばれるものになる!!
- 自分の子や孫で発生したエラーは親には伝播させません。
- 自分の親から伝播してきたキャンセルは、子や孫に伝播させます。
こちらはScopeCoroutine
の子クラス=AbstractCoroutine
の子クラスのようです。
こちらもScopeCoroutine
の子クラス=AbstractCoroutine
の子クラスのようです。
-
withContext
はCoroutineScope
のメソッドではありません。 -
launch
などと同様suspend fun
なのでCoroutineScope
の中でしか呼べません。 - 【
current corouitneContext
+ 引き数で渡されたcroutineContext
】(順番大事)として作ったCoroutineContext
をもつCoroutineScope
で引数として渡されたラムダ関数を実行します。 - ラムダ関数が終わるまで待って、結果を返します。
- ラムダ関数の中で例外が発生した場合、親のスコープには例外が伝播しません。
【例】
【実行結果 出力】
1
catch: java.lang.Exception: error
2
- 一つ目の
launch
と、withContext
のスコープに紐づいたCoroutineContext
のJob
には親子関係があり、またひとつめのlaunch
とふたつめのlaunch
を起動しているCoroutineScope
に紐づいたCoroutineContext
は同じものですが、エラーは伝播していません。 - エラーの伝播はしませんが、
withContext
から中でキャッチされなかった例外は外に投げられています。
SupervisorJob
SupervisorJob(parent: Job? = null)
※SupervisorJobImpl(parent: Job?)
-
1つの子で例外が発生したとしても他の子にキャンセルを伝播させたくない!という時に使える
Job
例 : WebAPIをたたいたレスポンスデータを表示させるリストと、ローカルDBのデータを表示するリストを持っている画面で、どっちかの取得時に例外が発生したとしてももう片方までキャンセルする必要はない。 -
子Jobがエラーしても他の子Jobにキャンセルを伝播させないJob
- 引数にJobを渡すとそのJobの子Jobになる。
- 伝播してきたキャンセルは自分の子に伝播する。
※この辺りは普通のJobと同じ
- SupervisorJobの子Jobは(普通にlaunchなどを使う場合は)SupervisorJobではない(孫Job同士はキャンセルが伝播する)ことに注意
【例】
【実行結果 】出力】
1
3
5
Exception in thread "DefaultDispatcher-worker-2 @coroutine#2" java.lang.RuntimeException: exception
at FileKt$main$2$2.invokeSuspend(File.kt:23)
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
【解説/捕捉】
① … ラムダが実行されるのはSupervisoJob
の子に紐づいたスコープ。キャンセルされない。
② … ラムダが実行されるのはSupervisoJob
の子に紐づいたスコープ。自分の子からエラーが伝播してきたエラーを受け取る。
③ … ラムダが実行されるのはSupervisoJob
の孫に紐づいたスコープ。片方で発生したエラーにより、もう片方もキャンセルする。(伝播している。)
CoroutineExceptionHandler
- CoroutineContextのひとつ。
- キャッチされなかった例外を処理できる(ことがある)。(詳しくは後述)
【例】
キャッチできなかった例外を処理できるとき
これが
【実行結果】
1
Exception in thread "DefaultDispatcher-worker-2 @coroutine#2" java.lang.RuntimeException: テスト
at FileKt$main$2.invokeSuspend(File.kt:16)
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)
こうなる
- エラーが伝播しないとかではない。
- 伝播させた後の処理についてカスタマイズできるだけ。
【実行結果】
1
例外キャッチ : java.lang.RuntimeException: テスト
※CoroutineExceptionHandlerを設定できるのは
コルーチンスコープそのものの作成時かルートのみ
= 途中で上書きしようとしても意味をなさない
【例】
【実行結果 出力】
1
例外キャッチ2 : java.lang.RuntimeException: テスト
キャッチされなかったエラーを処理できる「ことがある」とは?
launch
のなかで発生したエラーはハンドリングできるけど、async
のなかのものはハンドリングできません。
理由をみていきます。
そもそも、どうやってlaunch
はキャッチされない例外があった時にCoroutineExceptionHandler
に処理を渡しているのか
launch
で使われているCoroutineContext
であるStandaloneCoroutine
赤枠のhandleJobException
メソッドのoverrideがポイント
overrrride元 : JobSupport#handleJobException
- 「最後まで処理されなかったExceptionを処理します。処理した時はtrueを返します。」
- では、実際に処理している
handleCoroutineException
メソッドは何をしている?
① … CoroutineContext
がCoroutineExceptionHandler
要素を持つ場合は、それを呼び出して処理を委任
② … ない時や、CoroutineExceptionHandler
が例外を投げた時はグローバルな(デフォルトの)ハンドラーに処理を委任
→ CoroutineExceptionHandler
がキャッチされなかった例外をハンドリングできるか否かは
起動されたcoroutine
のJobSupport#handleJobException
の内容に左右される=coroutine
を起動するメソッドの実装に左右される
- では、
aync
の場合
async
で使用されているDefferedCoroutine
独自にJobSupport#handleJobException
のoverrideはしていないようです。
※DefferedCoroutine‘の親クラスである
AbstractCoroutineも[
JobSupport#handleJobException`はoverrideしていません。](https://github.com/Kotlin/kotlinx.coroutines/blob/1.5.2/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt#L37)
→
したがって、async
はCoroutineExceptionHandler
要素を持つCoroutineContext
に紐づいたCoroutineScope
で起動しても、CoroutineExceptionHandler
でキャッチされなかった例外はハンドリングできません!
コルーチンビルダー
コルーチンビルダーとは
新しいcoroutine
を起動するメソッド
例 : launch
、async
など
https://kotlinlang.org/docs/coroutines-basics.html#your-first-coroutine
- ここでは、特殊なコルーチンビルダーである
runBlocking
を紹介します。
runBlocking
-
CoroutineScope
のメソッドではない -
suspend function
でもない -
CoroutineScope
の中で呼んではいけない - 呼び出し側のスレッドを止めて、渡されたラムダ関数を実行します
- ラムダ関数は
CoroutineScope
で実行されます -
launch
で起動したものなど、子コルーチンのすべての終了を待つ - ラムダ関数内でキャッチされない例外が発生した場合、
runBlocking
を呼び出したスレッドに例外が伝播する(ようだ)
【例】
- 何もしなくても、起動されたコルーチンが終わるまで待つ
【実行結果 表示】
1
2
【例】
- 元Threadを止めます
- 値を返します
【実行結果 表示】
1
2
3
【例】
- キャッチされない例外が発生した場合、呼び出し元スレッドに例外を投げます。
【実行結果 表示】
1
Exception in thread "main" java.lang.RuntimeException: exception
at FileKt$main$blocking$1.invokeSuspend (File.kt:10)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run (DispatchedTask.kt:106)
【例】
- キャッチされない例外が発生した場合、呼び出し元のスレッド(だけ)に例外を投げます、
【実行結果 表示】
4
1
Exception in thread "Thread-0" java.lang.RuntimeException: exception
at FileKt$main$1$blocking$1.invokeSuspend(File.kt:11)
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)
5
スコープビルダー
スコープビルダーとは
- 新しい
CoroutineScope
を作成するメソッド
https://kotlinlang.org/docs/coroutines-basics.html#scope-builder
- ここでは2つ(
coroutineScope
、supervisorScope
)を紹介します。
couitneScope
- 挙動のほとんどは(
withContext
の時にも出てきた)ScopeCoroutine
に依存しています。→ScopeCoroutine
-
CoroutineScope
のメソッドではない -
suspend fun
なのでCoroutineScope
のなかでしか呼べません - 全ての子コルーチンの終了を待ちます
- 作成する
CoroutineScope
に紐づくCoroutineContext
は、current corouitneContext
を引き継ぐがJob
だけは必ず上書きします - しかしその
Job
はcurrent corouitneContext
のJob
を親とします
-→ -
ScopeCoroutine
を使用しているので、子や孫から伝播してきたエラーは親には伝播させません - 子でエラーが発生した場合、ほかの子はキャンセルします
【例】
- 全ての子コルーチンの終了を待ちます
【実行結果 表示】
1
2
3
4
5
6
7
【例】
- 子や孫から伝播してきたエラーは親には伝播させません
- 子でエラーが発生した場合、ほかの子はキャンセルします
① … 他の子でエラーが発生したので、delay
の間にキャンセルします
② … coroutineScope
の子でエラーが発生してもcoroutineScope
に紐づいたCoroutineContext
のJob
の親にエラーは伝播しないので、キャンセルされずに実行されます
【実行結果 表示】
1
8
2
3
4
5
catch: java.lang.RuntimeException: error
7
9
【例】
-
Job
を介して自分の親に例外を伝播させることはありませんが、キャッチされない例外が発生した場合coroutineScope
の外に例外を投げることはします
① … coroutineScope
が外に例外を投げる=ラムダ内でエラーが発生しました
② … ①のlaunch
はcoroutineScope
で失敗しているので、②は実行されません
② … 同じJob
に紐づいたCoroutineScope
から起動した①がエラーで失敗したので、delay
の間でキャンセルが伝播して最後のprintln("9")
は実行されません
【実行結果 表示】
1
2
8
3
4
5
Exception in thread "DefaultDispatcher-worker-2 @coroutine#1" java.lang.RuntimeException: error
at FileKt$main$1$1$1.invokeSuspend(File.kt:15)
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)
supervisorScope
- 挙動のほとんどは
SupervisorCoroutine
に依存しているようです。(赤枠の中)
- ベースは
ScopeCoroutine
で、childCancelled
だけoverrideされているようです。
-
CoroutineScope
のメソッドではない -
suspend fun
なのでCoroutineScope
のなかでしか呼べません - 全ての子コルーチンの終了を待ちます
- 作成する
CoroutineScope
に紐づくCoroutineContext
は、current corouitneContext
を引き継ぐがJob
だけは必ず上書きします
(※コメントには「SupervisorJob
で上書きする」、とありますが、コードを見る限りSupervisorJob
と同じようにchildCancelled
をoverrideしているJob
を使用指定はいるけどSupervisorJob
そのものを利用しているようには見えない…?) - しかしその
Job
はcurrent corouitneContext
のJob
を親とします -
ScopeCoroutine
を使用しているので、子や孫から伝播してきたエラーは親には伝播させません - 子でエラーが発生した場合、ほかの子はキャンセルしません
→ coroutineScope
の、上書きするJob
を`SupervisorJob‘にしたバージョンです。
【例】
① … 他の子でエラーが発生しても、キャンセルされません
② … supervisorScope
の子でエラーが発生してもsupervisorScope
に紐づいたCoroutineContext
のJob
の親にエラーは伝播しないので、キャンセルされずに実行されます
【実行結果 表示】
1
2
8
3
4
5
Exception in thread "DefaultDispatcher-worker-1 @coroutine#3" java.lang.RuntimeException: error
at FileKt$main$1$1$1.invokeSuspend(File.kt:16)
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)
6
7
9
【例】
- キャッチされない例外が発生した場合も
supervisorScope
の外に例外を投げることもしないようです。なぜかは分からず…。
※なので、ひとつ前の例でもtry-catch
でcatch
できていません
① … supervisorScope
は外に例外を投げないので、エラーは発生していません
② … 正常に実行されます
② … 同じJob
に紐づいたCoroutineScope
から起動した①はエラーなしで成功したので、最後のprintln("9")
も実行されます
【実行結果 表示】
1
2
8
3
4
5
Exception in thread "DefaultDispatcher-worker-1 @coroutine#3" java.lang.RuntimeException: error
at FileKt$main$1$1$1.invokeSuspend(File.kt:15)
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)
6
7
9
コルーチンビルダー、スコープビルダーの「全ての子コルーチンの終了を待つ」について
- 子コルーチンを起動する時に
CoroutineDispatcher
等を新しいものに上書きしても大丈夫だが、Job
を新しいものにすると待ってくれなくなります。(runBuilding
,coroutineScope
,supervisorScope
すべて) - ドキュメントにもコード上にも根拠は見つけられなったが、親子間のエラーの伝搬などがすべて
Job
に依存していることを考えると納得できる挙動ではある。
【例】
【実行結果 表示】
1
3
2
4
【例】
【実行結果 表示】
1
3
4
2
コルーチンビルダーとスコープビルダーの違いについて
※コルーチンビルダーとスコープビルダーの違いは、混とんとしているように見えます。
- 公式サイトの内容や各メソッドのコメントを見ると、
launch
やrunBlocking
はコルーチンビルダー/coroutineScope
やsupervisorScope
はスコープビルダーとして書かれているように思われますがlaunch
のなかで作成しているAbstractCoroutine
もCoroutineScope
を実装しています。
https://kotlinlang.org/docs/coroutines-basics.html#your-first-coroutine
https://kotlinlang.org/docs/coroutines-basics.html#scope-builder
-
final override val isScopedCoroutine : Boolean get() = true
なJob
やCoroutineScope
を使用しているものをスコープビルダーと呼ぶというのでもなさそうです。
(runBlocking
で使用しているBlockingCoroutineも、coroutineScope
やsupervisorScope
で使用しているScopeCoroutineもfinal override val isScopedCoroutine : Boolean get() = true
なので)
→結局、どこに着目して分類されているかに左右されている?
参考資料
https://kotlinlang.org/docs/coroutines-basics.html#scope-builder-and-concurrency
https://kotlinlang.org/docs/exception-handling.html#coroutineexceptionhandler
https://zenn.dev/at_sushi_at/books/edf63219adfc31
https://zenn.dev/wm3/articles/aa85d6cc7aa0a8146863#coroutine-builder-%E3%81%A8-coroutinescope
https://mahata.gitlab.io/post/2019-04-23-coroutines-kotlin/
https://speakerdeck.com/ntaro/kotlin-contracts-number-m3kt?slide=12
その他記事内のリンクのページなど