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

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

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

挙動を試すときは、[公式のonline Kotlin playground](https://play.kotlinlang.org/) を使用すると便利です。(`import`文を書けば)coroutineも使用できます。


[2022/4/10 追記]
Kotlin Coroutineに関するまとめの記事の第二弾を公開しました→[Kotlin Coroutine のキャンセル周りの話②](https://qiita.com/kkkkan/items/81dd11b03e8f9aabacff)

[2022/7/4 追記]
- 内容に誤りがあったため「[CoroutineContextとは](#`CoroutineContext`とは)」 「[CoroutineContextの合成](#`CoroutineContext`の合成)」の章を修正しました。
- ページ内リンクの貼り方が間違っていてクリックしても飛べない箇所があったので修正しました。


----------

# Kotlin Coroutine の基本

------


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

-------

## coroutineを起動するには
coroutineを起動するには[CoroutineScope](https://github.com/Kotlin/kotlinx.coroutines/blob/1.5.2/kotlinx-coroutines-core/common/src/CoroutineScope.kt#L78)が必要。

【例】

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

}
```

------


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

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

-------


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


------

## `CoroutineScope`のつくり方
[CoroutineContext](https://github.com/JetBrains/kotlin/blob/v1.5.31/libraries/stdlib/src/kotlin/coroutines/CoroutineContext.kt#L14)を指定すると作れます。


|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/1f417799-67bf-35f0-fdb0-d4b5b2c4d9ad.png)|
|:--|

------

【例】

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

------

## `CoroutineContext`とは
[2022/7/4 修正済み]
- `CoroutineScope`の挙動を決めるもの。
- 1つの`CoroutineScope`は必ず1つの`CoroutineContext`に紐づいています。
- interfaceです。
- 内部interfaceとして`Element`と`Key<e:Element>`を持ち、`Element`は`CoroutineContext`の子interfaceでもあります。
- 更に`Element`は`Key`を内部フィールドとして持ちます。
- `CoroutineContext.Element`は種類ごとに各々ユニークな[CoroutineContext.Key](https://github.com/JetBrains/kotlin/blob/v1.5.31/libraries/stdlib/src/kotlin/coroutines/CoroutineContext.kt#L54)を持っています。
- `CoroutineContext(.Element)`同士は加算可能で、同じ`Key`を持つもの同士を足した場合は最後に足したものが勝ち、違うKeyを持つものを足すと両立します(Mapのような動き)
- いろんな種類の[CoroutineContext.Element](https://github.com/JetBrains/kotlin/blob/v1.5.31/libraries/stdlib/src/kotlin/coroutines/CoroutineContext.kt#L59)のmapになっています。
- 実際の`CoroutineScope`の挙動は紐づいている`CoroutineContext`=「`CoroutineContext.Element`のmap」の中身により決まります。
- (`Job`や`CoroutineDispatcher`などは`CoroutineContext.Element`を実装しています。)

------


|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/089e0923-eb41-ca84-cff6-b697761956c6.png)|
|:--|

------

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/6b665372-61bc-80ea-d32a-d0d4938889d7.png)|
|:--|

 **※`CoroutineContext.Element`も`CoroutineContext`を実装しています。**

------


|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/708f4b23-e93b-753a-64f7-006dcea98f0c.png)|
|:--|

------

### `CoroutineContext`の代表的なもの
- [Job](https://github.com/Kotlin/kotlinx.coroutines/blob/1.5.2/kotlinx-coroutines-core/common/src/Job.kt#L112) : キャンセルや完了を待つときに使います。キャンセルやエラーの伝播も担当します。
- [CoroutineDispatcher](https://github.com/Kotlin/kotlinx.coroutines/blob/1.5.2/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt#L31) : 動くスレッドを決めます。
- [CoroutineName](https://github.com/Kotlin/kotlinx.coroutines/blob/1.5.2/kotlinx-coroutines-core/common/src/CoroutineName.kt#L14) : コルーチンの名前を設定できる。デバッグモードで使えるそうです。自分は使用したことがありません…
- [ CoroutineExceptionHandler](https://github.com/Kotlin/kotlinx.coroutines/blob/1.5.2/kotlinx-coroutines-core/common/src/CoroutineName.kt#L14) : CoroutineScopeのなかでtry-catchされなかった例外をハンドリングするためのもの。

------

## `CoroutineContext`の合成
[2022/7/4 修正済み]
- 前述の通り、`CoroutineContext(.Element)`は+演算子で加算できます。
- `Key`の重複を許さない規則のmapである「`CoroutineContext.Element`のmap」に別の`CoroutineContext(.Element`)を加算していくイメージ。
- +の右要素の`Key`要素が左要素に含まれていない時→両方保たれる
- +の右要素の`Key`要素が左要素に含まれている時  →右要素のものが採用される

------

【例】

```kotlin
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`に紐づいた`CoroutineContext`の`Job`同士に親子関係がある時にエラーやキャンセルが伝播します。

------

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

------

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/5d0f3785-4900-1688-3dc1-7f16d48756a4.png)|
|:--|

------

## `CoroutineScope#cancel` の実装
[CoroutineScope#cancel](https://github.com/Kotlin/kotlinx.coroutines/blob/1.5.2/kotlinx-coroutines-core/common/src/CoroutineScope.kt#L285)というメソッドがあります。その`CroutineScope`の子や孫などをすべてキャンセルできます。
このメソッドも、内部的にはその`CoroutineScope`に紐づいた`CoroutineContext`の`Job`要素に対してcancel()を呼んでいます。

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/3e2113d5-0f61-e740-a555-f6a73e010691.png)|
|:--|

------

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

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

------

### `Job`要素がない`CoroutineContext`から作成された`CoroutineScope`は？
`val scope = CoroutineScope([CoroutineContextのインスタンス])`と`CoroutineScope`のインスタンスを作成した時、内部的には[紐づけた`CoroutineContext`が`Job`要素を持たない時は新たに`Job`インスタンスを作成して渡されたCoroutineContextのインスタンスに加算しています。](https://github.com/Kotlin/kotlinx.coroutines/blob/1.5.2/kotlinx-coroutines-core/common/src/CoroutineScope.kt#L276)なのでこの場合も`CoroutineScope`に紐づいた`CoroutineContext`は`Job`要素を持ちます。


|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/c345e851-4b9c-4c46-8e21-1b2dd33be317.png)|
|:--|

------

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

------


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

【コード】
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/95540880-0768-757f-0e6c-a43bc05e984a.png)|
|:--|


https://pl.kotl.in/iy0CBs-pY?theme=darcula

------


【実行結果 出力】

```
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 1`で`delay`している時にキャンセルの伝播があったので、`try-catch`でcatchできています。
- もし、`coroutine 1`に`try-catch`がなければ、その場で`coroutine 1`はキャンセルされてしまい、`println("3")`は実行されません。

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

------


## `launch`の内部実装を見てみる

[CoroutineScope.launch](https://github.com/Kotlin/kotlinx.coroutines/blob/1.5.2/kotlinx-coroutines-core/common/src/Builders.common.kt#L47)

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/035507a6-b0c5-5ea0-b536-8f55f6f3a0d5.png)|
|:--|

------

### ① コード部分だけ拡大

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/41247307-0f57-941a-54c1-acfa4d93df8a.png)|
|:--|

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

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

------

### ② 引き続き`launch`の実装

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/8a1d0266-67f0-642a-8149-1598fe59ea9c.png)|
|:--|

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


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

------

### ③ `newCoroutineContext`の実装

[newCoroutineContext](https://github.com/Kotlin/kotlinx.coroutines/blob/1.5.2/kotlinx-coroutines-core/jvm/src/CoroutineContext.kt#L25)

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/3424f7db-39dc-1d0d-d052-c4af95406f56.png)|
|:--|



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

------


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

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/6ba5c16d-4901-83f1-c5a6-5f28c1503b40.png)|
|:--|

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

------


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

------

### ⑤ launchの実装に戻る

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

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/8a1d0266-67f0-642a-8149-1598fe59ea9c.png)|
|:--|

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

------

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

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/ec26eb9e-872b-466c-c246-d11a0d263cf2.png)|
|:--|

------

まず、6の`start.isLazy`が`true`か`false`かということですが、7で設定されている`start`のデフォルト値`CoroutineStart.DEFAULT`が`true` `false`どちらなのかを見てみたいと思います。
[CoroutineStart.DEFAULT](https://github.com/Kotlin/kotlinx.coroutines/blob/1.5.2/kotlinx-coroutines-core/common/src/CoroutineStart.kt#L37)
[CoroutineStart#isLazy](https://github.com/Kotlin/kotlinx.coroutines/blob/1.5.2/kotlinx-coroutines-core/common/src/CoroutineStart.kt#L122)

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

------

今回は、`start.isLazy == false`の時(8の囲いのところ)だけ見ていこうと思います。
(ちなみに`start.isLazy == true`の時に作成される`LazyStandaloneCoroutine`ですが、[こちらも`StandaloneCoroutine`を継承しており](https://github.com/Kotlin/kotlinx.coroutines/blob/1.5.2/kotlinx-coroutines-core/common/src/Builders.common.kt#L197)、キャンセルの伝播という点では`start.isLazy == false` の時と動きに違いはありません。)

------

### ⑥ `StandaloneCoroutine` の実装
[StandaloneCoroutine](https://github.com/Kotlin/kotlinx.coroutines/blob/1.5.2/kotlinx-coroutines-core/common/src/Builders.common.kt#L187)

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/667dc65b-4e65-b562-887d-6abea47216a7.png)|
|:--|

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

------


### ⑦ `AbstractCoroutine` の実装
[AbstractCoroutine](https://github.com/Kotlin/kotlinx.coroutines/blob/1.5.2/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt#L37)

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/75c7b813-ea9b-0067-47cf-f748ed3b8299.png)|
|:--|

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

------


|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/c44376e5-5775-715e-c5e6-5782093cedca.png)|
|:--|

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

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

------


**⑧ `JobSupport#initParentJob`の実装**

[JobSupport](https://github.com/Kotlin/kotlinx.coroutines/blob/1.5.2/kotlinx-coroutines-core/common/src/JobSupport.kt#L28) の定義
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/438be6f3-d26f-5cfe-0ef6-5c6a291fb32b.png)|
|:--|

`JobSupport`も`Job`の子クラスなことが分かります。

------

[JobSupport#initParentJob](https://github.com/Kotlin/kotlinx.coroutines/blob/1.5.2/kotlinx-coroutines-core/common/src/JobSupport.kt#L142)

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/83378256-7441-4c56-705d-bc67512e884d.png)|
|:--|

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/0e4ac47b-8114-73df-9c27-bb5a216500b7.png)|
|:--|

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

------


### ⑨ `Job#attachChild`メソッドの定義

[Job#attachChild](https://github.com/Kotlin/kotlinx.coroutines/blob/1.5.2/kotlinx-coroutines-core/common/src/Job.kt#L239)

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/a0bdd99f-69a3-4426-d221-1e59e3e54c40.png)|
|:--|

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

------


例えば、[`Job()`でインスタンス化したJobは、JobImplのインスタンスで、](https://github.com/Kotlin/kotlinx.coroutines/blob/1.5.2/kotlinx-coroutines-core/common/src/Job.kt)

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/54702196-336c-0b8d-f67a-2019799d79cf.png)|
|:--|

[JobImplはJobSupportの子クラスで、](https://github.com/Kotlin/kotlinx.coroutines/blob/1.5.2/kotlinx-coroutines-core/common/src/JobSupport.kt#L1315)
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/6366c092-732e-13fa-14a6-5ccab96de518.png)|
|:--|


[JobSupportでは、attachChildメソッドが実装されています。](https://github.com/Kotlin/kotlinx.coroutines/blob/1.5.2/kotlinx-coroutines-core/common/src/JobSupport.kt#L960)

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/ae1c2b66-de4b-959c-7407-15873e70cdc5.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#start`は`AbstractCoroutine#start`です。したがって、そちらの実装を見てみます。

[AbstractCoroutine#start](https://github.com/Kotlin/kotlinx.coroutines/blob/1.5.2/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt#L125)


|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/aa1bb0a0-09d5-5afe-5de8-9c21cc801c39.png)|
|:--|


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

------


[CoroutineStart#invoke](https://github.com/Kotlin/kotlinx.coroutines/blob/1.5.2/kotlinx-coroutines-core/common/src/CoroutineStart.kt#L108)


|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/fa10175c-6f1d-6c1a-41b3-831694efaae1.png)|
|:--|

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

------


#### おまけ

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

------

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


[(suspend (R) -> T)#startCoroutineCancellable](https://github.com/Kotlin/kotlinx.coroutines/blob/1.5.2/kotlinx-coroutines-core/common/src/intrinsics/Cancellable.kt#L25)


|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/45bcdead-002e-9bd2-b699-2c179f6b8d55.png)|
|:--|

------

この後は

[runSafety](https://github.com/Kotlin/kotlinx.coroutines/blob/1.5.2/kotlinx-coroutines-core/common/src/intrinsics/Cancellable.kt#L48)

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/4f3ba73e-b56c-7fe1-af5a-368c9a90e058.png)|
|:--|

[(suspend R.() -> T)#createCoroutineUnintercepted](https://github.com/JetBrains/kotlin/blob/v1.5.31/libraries/stdlib/jvm/src/kotlin/coroutines/intrinsics/IntrinsicsJvm.kt#L118)


|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/b332e85c-c7d8-f9ac-8ddc-9fb68cc2dbb0.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`呼び出し時に渡された`CoroutineContext`の`Job`要素または`launch`を起動した`CoroutineScope`に紐づいた`CoroutineContext`の`Job`**

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

が**例外を受け取ります。**

------


→

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

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


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

------

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

------


## コード例の場合のキャンセルの伝播の様子
[初めにlaunch内で例外が発生した時の実際のコード出力を見てみる](#初めにlaunch内で例外が発生した時の実際のコード出力を見てみる) で出したコード例

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/95540880-0768-757f-0e6c-a43bc05e984a.png)|
|:--|


https://pl.kotl.in/iy0CBs-pY?theme=darcula

------


`coroutine 3`で例外が発生
→`coroutine 3`で発生した例外を受け取るのは、【`coroutine 3`の`launch`を起動した`CoroutineScope`に紐づいた`CoroutineContext`の`Job`要素】=【`coroutine 2`に紐づいた`CoroutineContext`の`Job`要素】を親にしている`StandaloneCoroutine`
→親である【`launch`を起動した`CoroutineScope`に紐づいた`CoroutineContext`の`Job`要素】=【`coroutine 2`に紐づいた`CoroutineContext`の`Job`要素】にエラーが伝播
→受け取った【`coroutine 2`に紐づいた`CoroutineContext`の`Job`要素】は、【`coroutine 2`の`launch`を起動した`CoroutineScope`に紐づいた`CoroutineContext`の`Job`要素】=【`scope`に紐づいた`CoroutineContext`の`Job`要素】を親にしている`StandaloneCoroutine`
→【`scope`に紐づいた`CoroutineContext`の`Job`要素】にエラーが伝播
→もうひとつの【`scope`に紐づいた`CoroutineContext`の`Job`要素】を親にしている`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 3`の`CoroutineContext`自体にエラーが捕捉されずに到達してしまったので、バックスレッド(`coroutine 3`の`CoroutineContext`に紐づいている`Dispatchers.Default`スレッド)で例外が発生している旨のログが出ているのだと思われます。

------

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

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

------


[⑫まとめ](#まとめ)の内容の

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

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

------


### 方法①
`coroutine 3`で例外を`throw`する部分を`try-catch`で囲む

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/f57134d4-f618-591c-76e9-a6d11e991aa6.png)|
|:--|


https://pl.kotl.in/zjpaky0Of?theme=darcula

------


【実行結果 出力】

```
1
2
3
```

------


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

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/cbc34cfe-bfb6-4d6f-f17a-87db4020feff.png)|
|:--|

https://pl.kotl.in/Ksf9QradJ?theme=darcula

------


【実行結果 出力】

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

------


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

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/9869a9db-2355-7ddd-9f5c-70fa73651b74.png)|
|:--|

https://pl.kotl.in/LGUqi129n?theme=darcula

------

【実行結果 出力】

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

------

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

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245217/91f996ff-84fd-d628-0970-df4e866e81a8.png)|
|:--|

https://pl.kotl.in/WCiuPD8IK?theme=darcula

------

【実行結果 出力】

```
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 3`の`CoroutineContext`自体にエラーが捕捉されずに到達してしまうのは変わらないのでバックスレッド(`coroutine 3`の`CoroutineContext`に紐づいている`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 の公式サイトなど