1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Kotlin Coroutine のキャンセル周りの話②

Last updated at Posted at 2022-04-10
1 / 94

Kotlin Coroutine について勉強したことのまとめ第二弾。

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


環境/前提

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

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


asyncwithContextのキャンセルの伝播の仕様


第一弾でlaunchのコードを追ったときのように、asyncwithContextも軽く内部実装やコメントを見つつメソッドの仕様やキャンセルの伝播の仕様を紹介します。


aync

CorouitneScope#async

image.png

launchを追ったときの教訓よりデフォルトはstart.isLazyfalseです。
したがって、DeferredCoroutineの実装を見てみます。

DeferredCoroutine

image.png

DeferredCoroutinelaunchを追ったときにも出てきたAbstractCoroutineの子クラスのようです。
また、その他asyncの内部実装に使われているnewCoroutineContextメソッドやAbstaractCoroutine#startを呼ぶ構造もlaunchと一緒です。

よって、launchを追ったときの結果から、
asyncに引数として渡されたラムダ関数の中で例外が発生した場合は,
async呼び出し時に渡されたCoroutineContextJob要素またはasyncを起動したCoroutineScopeに紐づいたCoroutineContextJob
を親にしたCoroutineScopeかつJobであるDeferredCoroutine
例外を受け取ります。


※ちなみに、start.isLazytrueの時に使われているLazyDeferredCoroutineは、DeferredCoroutineの子クラスです。

LazyDeferredCoroutine

image.png

asyncのコード例と実行結果です。

【例】

image.png


【実行結果 出力】

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 4Jobまで伝わりキャンセルされるので、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節は呼ばれないし、

【例】

image.png


【実行結果 出力】

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節で例外を捕捉できます。そしてキャンセルの伝播自体は起きます。

【例】

image.png


【実行結果 出力】

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

withContext

image.png
image.png

※長いのでスクリーンショットがコメント部と実装部の2枚になりました。


① … 現在のCoroutineContextに、引数で渡されたCoroutineContextのインスタンスを加算しています。(順番大事)


② … ScopeCoroutine

image.png

こちらもAbstractCoroutineの子クラスのようです。

ただし、launchasyncとの違いとして、赤く囲われた部分が重大なポイントになっています。


final override val isScopedCoroutine : Boolean get() = true とは?


overrideしている元はどこ?
JobSupportで定義されています。
AbstractCoroutineJobSupportを継承しています。

image.png

isScopedCoroutinetrueになるときは、例外をそれより内側のスコープにしか投げない、scoped coroutineと呼ばれるものになる!!

  • 自分の子や孫で発生したエラーは親には伝播させません。
  • 自分の親から伝播してきたキャンセルは、子や孫に伝播させます。

③ … UndispatchedCoroutine

image.png

こちらはScopeCoroutineの子クラス=AbstractCoroutineの子クラスのようです。


④ … DispatchedCoroutine

image.png

こちらもScopeCoroutineの子クラス=AbstractCoroutineの子クラスのようです。


  • withContextCoroutineScopeのメソッドではありません。
  • launchなどと同様suspend funなのでCoroutineScopeの中でしか呼べません。
  • current corouitneContext + 引き数で渡されたcroutineContext】(順番大事)として作ったCoroutineContextをもつCoroutineScopeで引数として渡されたラムダ関数を実行します。
  • ラムダ関数が終わるまで待って、結果を返します。
  • ラムダ関数の中で例外が発生した場合、親のスコープには例外が伝播しません。

【例】

image.png


【実行結果 出力】

1
catch: java.lang.Exception: error
2

  • 一つ目のlaunchと、withContextのスコープに紐づいたCoroutineContextJobには親子関係があり、またひとつめのlaunchとふたつめのlaunchを起動しているCoroutineScopeに紐づいたCoroutineContextは同じものですが、エラーは伝播していません。
  • エラーの伝播はしませんが、withContextから中でキャッチされなかった例外は外に投げられています。

SupervisorJob


SupervisorJob(parent: Job? = null)

image.png

SupervisorJobImpl(parent: Job?)

image.png

  • 1つの子で例外が発生したとしても他の子にキャンセルを伝播させたくない!という時に使えるJob
    例 : WebAPIをたたいたレスポンスデータを表示させるリストと、ローカルDBのデータを表示するリストを持っている画面で、どっちかの取得時に例外が発生したとしてももう片方までキャンセルする必要はない。

  • 子Jobがエラーしても他の子Jobにキャンセルを伝播させないJob

image.png

  • 引数にJobを渡すとそのJobの子Jobになる。
  • 伝播してきたキャンセルは自分の子に伝播する。

※この辺りは普通のJobと同じ

image.png

  • SupervisorJobの子Jobは(普通にlaunchなどを使う場合は)SupervisorJobではない(孫Job同士はキャンセルが伝播する)ことに注意
image.png

【例】

image.png


【実行結果 】出力】

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

【解説/捕捉】

image.png

① … ラムダが実行されるのはSupervisoJobの子に紐づいたスコープ。キャンセルされない。
② … ラムダが実行されるのはSupervisoJobの子に紐づいたスコープ。自分の子からエラーが伝播してきたエラーを受け取る。
③ … ラムダが実行されるのはSupervisoJobの孫に紐づいたスコープ。片方で発生したエラーにより、もう片方もキャンセルする。(伝播している。)


CoroutineExceptionHandler


CoroutineExceptionHandler

  • CoroutineContextのひとつ。
  • キャッチされなかった例外を処理できる(ことがある)。(詳しくは後述)
image.png

【例】

キャッチできなかった例外を処理できるとき


これが

image.png

【実行結果】

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)

こうなる 

  • エラーが伝播しないとかではない。
  • 伝播させた後の処理についてカスタマイズできるだけ。
image.png

【実行結果】

1
例外キャッチ : java.lang.RuntimeException: テスト

※CoroutineExceptionHandlerを設定できるのは
コルーチンスコープそのものの作成時かルートのみ

= 途中で上書きしようとしても意味をなさない


【例】

image.png

【実行結果 出力】

1
例外キャッチ2 : java.lang.RuntimeException: テスト

キャッチされなかったエラーを処理できる「ことがある」とは?


launchのなかで発生したエラーはハンドリングできるけど、asyncのなかのものはハンドリングできません。


理由をみていきます。


そもそも、どうやってlaunchはキャッチされない例外があった時にCoroutineExceptionHandlerに処理を渡しているのか


launchで使われているCoroutineContextであるStandaloneCoroutine

image.png

赤枠のhandleJobExceptionメソッドのoverrideがポイント


overrrride元 : JobSupport#handleJobException

  • 「最後まで処理されなかったExceptionを処理します。処理した時はtrueを返します。」
image.png

  • では、実際に処理しているhandleCoroutineExceptionメソッドは何をしている?

handleCoroutineException

image.png

① … CoroutineContextCoroutineExceptionHandler要素を持つ場合は、それを呼び出して処理を委任
② … ない時や、CoroutineExceptionHandlerが例外を投げた時はグローバルな(デフォルトの)ハンドラーに処理を委任


CoroutineExceptionHandlerがキャッチされなかった例外をハンドリングできるか否かは
起動されたcoroutineJobSupport#handleJobExceptionの内容に左右される=coroutineを起動するメソッドの実装に左右される


  • では、ayncの場合

asyncで使用されているDefferedCoroutine

image.png

独自に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)



したがって、asyncCoroutineExceptionHandler要素を持つCoroutineContextに紐づいたCoroutineScopeで起動しても、CoroutineExceptionHandlerでキャッチされなかった例外はハンドリングできません!


コルーチンビルダー


コルーチンビルダーとは


新しいcoroutineを起動するメソッド
例 : launchasyncなど

https://kotlinlang.org/docs/coroutines-basics.html#your-first-coroutine

  • ここでは、特殊なコルーチンビルダーであるrunBlockingを紹介します。

runBlocking


runBlocking

image.png

  • CoroutineScopeのメソッドではない
  • suspend functionでもない
  • CoroutineScopeの中で呼んではいけない
  • 呼び出し側のスレッドを止めて、渡されたラムダ関数を実行します
  • ラムダ関数はCoroutineScopeで実行されます
  • launchで起動したものなど、子コルーチンのすべての終了を待つ
  • ラムダ関数内でキャッチされない例外が発生した場合、runBlockingを呼び出したスレッドに例外が伝播する(ようだ)

【例】

  • 何もしなくても、起動されたコルーチンが終わるまで待つ
image.png


【実行結果 表示】

1
2

【例】

  • 元Threadを止めます
  • 値を返します
image.png


【実行結果 表示】

1
2
3

【例】

  • キャッチされない例外が発生した場合、呼び出し元スレッドに例外を投げます。
image.png


【実行結果 表示】

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) 


【例】

  • キャッチされない例外が発生した場合、呼び出し元のスレッド(だけ)に例外を投げます、
image.png


【実行結果 表示】

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つ(coroutineScopesupervisorScope)を紹介します。

couitneScope


coroutineScope

  • 挙動のほとんどは(withContextの時にも出てきた)ScopeCoroutineに依存しています。→ScopeCoroutine
image.png

  • CoroutineScopeのメソッドではない
  • suspend fun なのでCoroutineScopeのなかでしか呼べません
  • 全ての子コルーチンの終了を待ちます
  • 作成するCoroutineScopeに紐づくCoroutineContextは、current corouitneContextを引き継ぐがJobだけは必ず上書きします
  • しかしそのJobcurrent corouitneContextJobを親とします
    -→
  • ScopeCoroutineを使用しているので、子や孫から伝播してきたエラーは親には伝播させません
  • 子でエラーが発生した場合、ほかの子はキャンセルします

【例】

  • 全ての子コルーチンの終了を待ちます
image.png


【実行結果 表示】

1
2
3
4
5
6
7

【例】

  • 子や孫から伝播してきたエラーは親には伝播させません
  • 子でエラーが発生した場合、ほかの子はキャンセルします
image.png

① … 他の子でエラーが発生したので、delayの間にキャンセルします
② … coroutineScopeの子でエラーが発生してもcoroutineScopeに紐づいたCoroutineContextJobの親にエラーは伝播しないので、キャンセルされずに実行されます


【実行結果 表示】

1
8
2
3
4
5
catch: java.lang.RuntimeException: error
7
9

【例】

  • Jobを介して自分の親に例外を伝播させることはありませんが、キャッチされない例外が発生した場合coroutineScopeの外に例外を投げることはします
image.png

① … coroutineScopeが外に例外を投げる=ラムダ内でエラーが発生しました
② … ①のlaunchcoroutineScopeで失敗しているので、②は実行されません
② … 同じ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


supervisorScope

  • 挙動のほとんどはSupervisorCoroutineに依存しているようです。(赤枠の中)
image.png

SupervisorCoroutine

  • ベースはScopeCoroutineで、childCancelledだけoverrideされているようです。
image.png

  • CoroutineScopeのメソッドではない
  • suspend fun なのでCoroutineScopeのなかでしか呼べません
  • 全ての子コルーチンの終了を待ちます
  • 作成するCoroutineScopeに紐づくCoroutineContextは、current corouitneContextを引き継ぐがJobだけは必ず上書きします
    (※コメントには「SupervisorJobで上書きする」、とありますが、コードを見る限りSupervisorJobと同じようにchildCancelledをoverrideしているJobを使用指定はいるけどSupervisorJobそのものを利用しているようには見えない…?)
  • しかしそのJobcurrent corouitneContextJobを親とします
  • ScopeCoroutineを使用しているので、子や孫から伝播してきたエラーは親には伝播させません
  • 子でエラーが発生した場合、ほかの子はキャンセルしません

coroutineScopeの、上書きするJobを`SupervisorJob‘にしたバージョンです。


【例】

image.png

① … 他の子でエラーが発生しても、キャンセルされません
② … supervisorScopeの子でエラーが発生してもsupervisorScopeに紐づいたCoroutineContextJobの親にエラーは伝播しないので、キャンセルされずに実行されます


【実行結果 表示】

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-catchcatchできていません
image.png

① … 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に依存していることを考えると納得できる挙動ではある。

【例】

image.png


【実行結果 表示】

1
3
2
4

【例】

image.png


【実行結果 表示】

1
3
4
2

コルーチンビルダーとスコープビルダーの違いについて


※コルーチンビルダーとスコープビルダーの違いは、混とんとしているように見えます。

  • 公式サイトの内容や各メソッドのコメントを見ると、launchrunBlockingはコルーチンビルダー/coroutineScopesupervisorScopeはスコープビルダーとして書かれているように思われますがlaunchのなかで作成しているAbstractCoroutineCoroutineScopeを実装しています。

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() = trueJobCoroutineScopeを使用しているものをスコープビルダーと呼ぶというのでもなさそうです。

(runBlockingで使用しているBlockingCoroutineも、coroutineScopesupervisorScopeで使用しているScopeCoroutinefinal 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
その他記事内のリンクのページなど

1
2
0

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
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?