Kotlin Coroutinesの制御・キャンセル・例外処理を支える技術 ー Structured Concurrencyの内部実装
本記事は、Kotlin Coroutinesの基礎をすでに理解している中級者以上の開発者を対象とし、さらに理解を深めることを目的としたシリーズの一部です。
今回は、Kotlin Coroutinesの制御・キャンセル・例外処理を支える重要な概念である、Structured Concurrencyについて解説します。
Structured Concurrencyの歴史と設計思想
まずは、Structured Concurrencyが、どのような背景のもと、どのような課題を解決するために生まれたのか、を説明します。
これは、なぜKotlin Coroutinesの仕様を理解する上で、Structured Concurrencyの設計思想を理解することが不可欠であるためです。
Martin Sústrikによる創出 (2016年)
"Structured Concurrency"という用語は、2016年にMartin Sústrik[3]が生み出したとされています[4]。Martin氏は、C言語でStructured Concurrencyを実現するためのライブラリであるlibdill[5]を公開しました。
libdillのドキュメント上には、Martin氏の考えるStructured Concurrencyのコンセプトが説明されています[6]
このドキュメントでは、親コルーチンが子コルーチンを起動した場合、子コルーチンが完了するまで親コルーチンが完了しないことがStructured Concurrencyの本質とされています。
Structured Concurrencyに従った子コルーチンの起動
一方で、親コルーチンの完了後に、子コルーチンが残存するような場合は、Structured Concurrencyを違反します。例えば、Javaでスレッドをforkした後、joinせずに起動元のスコープを抜けた場合には、このような状況になり得ます。
Structured Concurrencyに反する子コルーチンの起動
Structured Concurrencyにおいては、親コルーチンの完了時には、子コルーチンも必ず完了していることから、リソースのリークを防止できるという大きなメリットがあります。
Nathaniel J. Smithによる普及 (2018年)
その後、2018にNathaniel J. Smith[7]は、PythonでStructured Concurrencyを実現するためのライブラリであるTrio[8]を作成するとともに、Structured Concurrencyの有益性を主張するブログ記事 "Notes on structured concurrency, or: Go statement considered harmful" [9]を公開しました。彼のブログ記事は、Structured Concurrencyの設計思想を広めることに貢献しました。
Nathanielは、構造化プログラミング (Structured Programming) とのアナロジーで、Structured Concurrencyを導入すべき理由を説明しています。
1960年台に遡ると、Edsger W. Dijkstraは構造化プログラミングを提唱し、goto構文を否定する有名な論文 "Go To statement considered harmful" を公表しました[10]。
構造化プログラミングとは、「順次 (順番に処理を進める)」「選択 (分岐)」「反復 (ループ)」の3つの制御構造のみからプログラムを構成する手法です[11]。
構造化プログラミングは今やスタンダードな考え方となっており、現代的なプログラミング言語でgoto構文がサポートされる自体が稀でしょう。
しかしながら、並行処理に目を向けると、goto構文に相当するような、制御フローを壊しうる設計パターンが今もなお広く使われている、というのがNathaniel氏の主張です。このような危険なパターンの代表例として、Go言語のgo
文が挙げられています。
結論として、Structured Concurrencyが普及することで、構造化プログラミングによってプログラムの明確さ・安全性が得られたのと同様のパラダイムシフトとなるとされています。
Kotlin Coroutinesへの実装 (2018年)
Kotlinは、メジャーなプログラミング言語としては、Structured Concurrencyを初めて公式の標準ライブラリでサポートした言語と言えます。
Kotlin Coroutinesにおいては、CoroutineScopeという概念によって、Structured Concurrencyを実現しています。
CoroutineScope内でコルーチンが起動されると、そのスコープ内で起動された全ての子コルーチンが完了するまで、CoroutineScope自体 (= 親コルーチン) は完了しません。
import kotlinx.coroutines.*
suspend fun main() {
coroutineScope { // CoroutineScopeを作成
launch { // CoroutineScope内で、コルーチンを起動
delay(100)
println("Delay finished.")
}
}
println("All finished.")
}
出力:
Delay finished.
All finished.
また、コルーチンを起動するための関数 (= Coroutine Builder) は、CoroutineScopeをレシーバとする拡張関数として定義されています。すなわち、すべてのコルーチンはCoroutineScopeを起点として起動する必要があります。
この制約によって、Kotlin Coroutinesの利用者は、Structured Concurrencyの本質である「子コルーチンが全て完了するまで、親コルーチンが完了しない」という仕様に、半ば強制的に従うこととなります。
以上、Structured Concurrencyの設計思想と、その背景を振り返りました。
Structured Concurrencyは、Kotlin Coroutinesにおいて、並行処理を明瞭かつ安全に記述することに貢献しています。
一方で、十分に理解しきれていない人にとっては、むしろ複雑あるいは分かりにくいと感じさせる挙動も生み出しています。例えば、キャンセル処理・例外処理の流れを難しいと感じている方は少なくないのではないでしょうか。
そこで以降の章では、Kotlin Coroutinesにおいて、Structured Concurrency ー すなわち子コルーチンの完了を待つという仕様 ーが、どのように実現されているかを解明していきます。
子コルーチンの完了を待つ仕組み
Kotlin Coroutinesが、「子コルーチンが全て完了するまで、親コルーチンが完了しない」というStrucured Concurrencyの本質をどのように実現しているかを、内部実装から解明していきます。
この仕様を実現してする上で重要なのが「CoroutineScope」です。
CoroutineScopeは、内部で起動された子コルーチンの完了を待機してから、CoroutineScope自体のスコープを抜けます。
以下のサンプルコードを実行して確かめてみましょう。
ここで使用されているcoroutineScope
はCoroutineScopeを作成するためのメソッドです。また、launch
はCoroutineScope内で子コルーチンを起動するためのメソッドです。
コードを実行すると、外側のcoroutineScope
のスコープが、内部でlaunch
された子コルーチンの完了を待つため、"All finished."というメッセージが最後にprintされます。
import kotlinx.coroutines.*
suspend fun main() {
coroutineScope { // CoroutineScopeを作成
launch { // CoroutineScope内で、コルーチンを起動
delay(100)
println("Delay finished.")
}
}
println("All finished.")
}
出力:
Delay finished.
All finished.
coroutineScope
が子コルーチンの完了を待つ仕組み
では、CoroutineScopeを作成しているcoroutineScope
メソッドの内部実装を追うことで、「子コルーチンの完了を待つ」仕組みがどのように実現されているのかを解明していきましょう。
coroutineScope
のソースコードを以下に抜粋します[13]。
なお、今回の説明に関係しないコードは、省略しています。
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
return suspendCoroutineUninterceptedOrReturn { uCont ->
val coroutine = ScopeCoroutine(uCont.context, uCont)
coroutine.startUndispatchedOrReturn(coroutine, block)
}
}
coroutineScope
内では、以下の3つのステップで処理を行っています。
-
suspendCoroutineUninterceptedOrReturn
で、呼び出し元のContinuation
を取得する。 - CoroutineScopeに紐づくコルーチンである、
ScopeCoroutine
を作成する。 -
ScopeCoroutine
をstartUndispatchedOrReturn
で起動し、CoroutineScope内の処理を実行する。
1.のステップで登場するContinuation
やsuspendCoroutineUninterceptedOrReturn
について、初めて聞いた読者も多いかもしれません。
これらの用語を簡単に説明します。
-
Continuation
とは、コルーチン (suspend関数) の、中断・再開の状態を管理するインスタンス。Continuation
に対してresumeWith
メソッドを呼び出すことで、呼び出し元の処理を再開する (今回の場合はcoroutineScope
のスコープを抜ける)。 -
suspendCoroutineUninterceptedOrReturn
とは、"呼び出し元" (今回の場合coroutineScope
の呼び出し元) のContinuation
を取得するためのメソッド。
suspend関数がbytecodeへとコンパイルされると、その引数には呼び出し元のContinuation
が追加されます。つまり、コンパイル後は、以下のようなbytecodeとなります。
public suspend fun <R> coroutineScope(
block: suspend CoroutineScope.() -> R,
continuation: Continuation<*>): R {
/* 省略 */
}
このコンパイル後に追加されるContinuation
を、コンパイル前のソースコード上で参照するために用いられるのが、suspendCoroutineUninterceptedOrReturn
メソッドです。
coroutineScope
メソッドの説明に戻ります。
coroutineScope
内では、suspendCoroutineUninterceptedOrReturn
によって呼び出し元のContinuation
を取得し、そのContinuation
が持つ呼び出し元の情報 ( CoroutineContext
) を使って、ScopeCoroutine
を作成・起動しています。
// 呼び出し元の`Continuation`を取得
return suspendCoroutineUninterceptedOrReturn { uCont ->
// 呼び出し元の`CoroutineContext`と`Continuation`から、`ScopeCoroutine`を作成
val coroutine = ScopeCoroutine(uCont.context, uCont)
// `ScopeCoroutine`を起動し、`block`を実行
coroutine.startUndispatchedOrReturn(coroutine, block)
}
ここまでの流れを、図にまとめます。
coroutineScope
メソッドの処理
では、ここで作成されているScopeCoroutine
とは、一体何でしょうか?
ScopeCoroutine
とは
ScopeCoroutine
は、CoroutineScopeに紐づくコルーチンです。
一般的には、Kotlin Coroutinesにおいて、コルーチンはlaunch
やasync
、runBlocking
のようなCoroutine Builderから起動されると言われます。ただ、内部的には、CoroutineScopeもそれ自体のコルーチンを作ります。
続いて、ScopeCoroutine
の初期化時の引数として渡されている、CoroutineContext
とContinuation
という2つのパラメータの役割を確認します。
これら2つは、「子コルーチンの完了を待つ」というStructured Concurrencyの定義を実現する上で不可欠な役割を果たしています。
ScopeCoroutine
に渡されたCoroutineContext
の役割
まず、CoroutineContext
の役割を見ていきます。結論から述べると、これは「コルーチンの親子構造の形成」という重要な役割を持ちます。
ScopeCoroutine
のソースコードを以下に抜粋します[15]。
/**
* This is a coroutine instance that is created by [coroutineScope] builder.
*/
internal open class ScopeCoroutine<in T>(
context: CoroutineContext,
@JvmField val uCont: Continuation<T>
) : AbstractCoroutine<T>(context, true, true), CoroutineStackFrame {
/* 実装は省略 */
}
ScopeCoroutine
の初期化時に渡されたcontext
パラメータは、基底クラスであるAbstractCoroutine
の初期化パラメータとして使用されています。
AbstractCoroutine
のソースコードから、特に関係する部分を抜粋します[16]
public abstract class AbstractCoroutine<in T>(
parentContext: CoroutineContext,
initParentJob: Boolean,
active: Boolean
) : JobSupport(active), Job, Continuation<T>, CoroutineScope {
init {
/*
* Setup parent-child relationship between the parent in the context and the current coroutine.
* It may cause this coroutine to become _cancelling_ if the parent is already cancelled.
* It is dangerous to install parent-child relationship here if the coroutine class
* operates its state from within onCancelled or onCancelling
* (with exceptions for rx integrations that can't have any parent)
*/
if (initParentJob) initParentJob(parentContext[Job])
}
init
内で、JobSupport
(基底クラス) のinitParentJob
メソッドが呼ばれています。
initParentJob
内で、呼び出し元 (親コルーチン) と、新たに作られたScopeCoroutine
との間に、親子構造が形成されます。
Jobの親子構造の形成
ここで登場するJob
とは、コルーチンのライフサイクルやコルーチン間の親子構造を管理するものです。Job
は、Structured Concurrencyを実現する上で、非常に重要なパーツです。
ソースコード上は、Job
はinterfaceであり、具体的な実装はJobSupport
クラスに存在します。
Structured Concurrencyに関わる多くの処理 (状態管理・キャンセル処理・例外処理など) が、JobSupport
クラス内で実装されているため、このクラスは以降頻出します。
では、Jobの親子構造の形成を担っている、JobSupport
クラスのinitParentJob
の内部実装を見ていきます。
JobSupport
のinitParentJob
メソッドのソースコードを以下に示します[17]。
protected fun initParentJob(parent: Job?) {
assert { parentHandle == null }
if (parent == null) {
parentHandle = NonDisposableHandle
return
}
parent.start() // make sure the parent is started
// ここでJobの親子関係を形成している。
val handle = parent.attachChild(this)
parentHandle = handle
if (isCompleted) {
handle.dispose()
parentHandle = NonDisposableHandle // release it just in case, to aid GC
}
}
着目して頂きたいのは、parent.attachChild(this)
の部分です。
ここで、親のJob
に対して、このコルーチンに紐づくJob
をattachする (取り付ける) ことで、親子構造を形成しています。
また、attachChild
メソッドからはChildHandle
[18]というオブジェクトが返されます。このオブジェクトは、子Job
のparentHandle
というフィールドに保持されます。
このChildHandle
とは、子コルーチンのJobから、親コルーチンのJobを参照するためのハンドラです。
後のセクションで触れますが、子コルーチンからキャンセル処理・例外処理を親コルーチンに伝える際には、このChildHandle
が使用されます。
:::messages
initParentJob
によって親子構造を形成する処理は、あらゆるコルーチンの基底クラスであるAbstractCoroutine
から呼ばれています。そのため、ScopeCoroutine
に限らず、あらゆるコルーチンの初期化時に、同様のことが行われます。
例えば、後ほどlaunch
によってコルーチンが起動される際の内部実装も説明しますが、その際にも「起動元のCoroutineScopeのコルーチン」と「launch
で起動された子コルーチン」の間の親子関係の形成するために、ここで説明したJobSupport
のinitParentJob
が使われています。
:::
ここまでで、ScopeCoroutine
に渡されたCoroutineContext
の役割を見てきました。
その重要な役割の一つに、呼び出し元の親コルーチンと、新しく作られたJob
の親子構造を形成することがあると分かりました。
Job
の親子構造の形成
続いて、もう一方の、ScopeCoroutine
に渡されたContinuation
の役割を見ていきましょう。
結論から述べると、CoroutineContext
は「親子構造の形成」を担ったのに対し、Continuation
はその親子構造に依存して「子コルーチンの実行を待つ」という役割を担います。
ScopeCoroutine
に渡されたContinuation
の役割
ScopeCoroutine
の初期化時に渡されたContinuation
は、uCont
フィールドとして、ScopeCoroutine
内に保持されます。
ScopeCoroutine
のソースコード[19]から、Continuation
に関わる部分を抜粋します。
internal open class ScopeCoroutine<in T>(
context: CoroutineContext,
@JvmField val uCont: Continuation<T> // unintercepted continuation
) : AbstractCoroutine<T>(context, true, true), CoroutineStackFrame {
override fun afterCompletion(state: Any?) {
uCont.intercepted().resumeCancellableWith(recoverResult(state, uCont))
}
override fun afterResume(state: Any?) {
uCont.resumeWith(recoverResult(state, uCont))
}
}
渡されたContinuation
は、afterCompletion
とafterResume
の2箇所で使用されており、それぞれresumeCancellableWith
あるいはresumeWith
が呼ばれています。
これらのメソッドは、挙動に違いはあるものの、呼び出し元のContinuation
に制御を戻す、すなわちcoroutineScope
自体のスコープを抜けるタイミングで呼ばれる、という点では共通しています。
つまり、これらのメソッドが呼ばれるまでの経路を知ることで、Structured Concurrencyの本質である「子コルーチンの完了を待つ」ことの仕組みを理解することに繋がります。
ScopeCoroutine
の起動
先述したafterCompletion
あるいはafterResume
は、ScopeCoroutine
の実行の最終段階で呼ばれるものと考えられます。
そこで、CoroutineScopeに紐づくScopeCoroutine
の起動から完了までの流れを追うことで、どのような経路でafterCompletion
あるいはafterResume
の呼び出しに繋がるのかを解明しましょう。
coroutineScope
のソースコードを、以下に再掲します。
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return suspendCoroutineUninterceptedOrReturn { uCont ->
val coroutine = ScopeCoroutine(uCont.context, uCont)
// `ScopeCoroutine`の起動
coroutine.startUndispatchedOrReturn(coroutine, block)
}
}
CoroutineScopeに紐づくScopeCoroutine
は、startUndispatchedOrReturn
メソッドによって起動されます。
startUndispatchedOrReturn
ならびにstartUndispatched
のソースコードを以下に示します[20]。
internal fun <T, R> ScopeCoroutine<T>.startUndispatchedOrReturn(
receiver: R, block: suspend R.() -> T
): Any? = startUndispatched(alwaysRethrow = true, receiver, block)
private fun <T, R> ScopeCoroutine<T>.startUndispatched(
alwaysRethrow: Boolean,
receiver: R, block: suspend R.() -> T
): Any? {
val result = try {
block.startCoroutineUninterceptedOrReturn(receiver, this)
} catch (e: DispatchException) {
dispatchExceptionAndMakeCompleting(e)
} catch (e: Throwable) {
CompletedExceptionally(e)
}
if (result === COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
val state = makeCompletingOnce(result)
if (state === COMPLETING_WAITING_CHILDREN) return COROUTINE_SUSPENDED
afterCompletionUndispatched()
return if (state is CompletedExceptionally) {
when {
alwaysRethrow || notOwnTimeout(state.cause) -> throw recoverStackTrace(state.cause, uCont)
result is CompletedExceptionally -> throw recoverStackTrace(result.cause, uCont)
else -> result
}
} else {
state.unboxState()
}
}
まず、、引数として渡されたblock
(これはcoroutineScope
の内部の処理) を、実行スレッド上でそのまま実行しています。その実行結果 (result
変数) に応じて、異なるハンドリングを行っています。
block
の実行結果は、3パターンに分かれます。それぞれどのようにハンドリングされるかを見ていきます。
coroutineScope
に渡されたblock
が中断された場合
パターン1. パターン1に相当するコードを示します。
coroutineScope {
delay(100) // suspension point: ここで実行が一時中断される。
launch { delay(1000) }
}
このようにcoroutineScope
内にsuspension pointが存在する場合 (ここではsuspend関数であるdelay
が存在する)、ここで処理が中断されます。
この場合、block
の実行は中断されているため、result
はCOROUTINE_SUSPENDED
となります。その結果、startUndispatched
では、coroutineScope
のblock
を実行するのみで早期returnすることとなります。
val result = try {
block.startCoroutineUninterceptedOrReturn(receiver, this)
} catch (e: DispatchException) {
dispatchExceptionAndMakeCompleting(e)
} catch (e: Throwable) {
CompletedExceptionally(e)
}
if (result === COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
その後、coroutineScope
のblock
が完了したタイミングで、coroutineScopeに紐づくコルーチン (ScopeCoroutine
) に対して、非同期的にresumeWith
が呼ばれます。
resumeWith
のソースコードを以下に示します[21]。これは、基底クラスのAbstractCoroutine
に存在します。
public final override fun resumeWith(result: Result<T>) {
val state = makeCompletingOnce(result.toState())
if (state === COMPLETING_WAITING_CHILDREN) return
afterResume(state)
}
ここで登場しているmakeCompletingOnce
は、コルーチンの完了処理を進めるためのメソッドです。パターン2でもmakeCompletingOnce
は利用されるため、後ほど詳細に解説します。
makeCompletingOnce
の呼び出し後の流れは2パターンに分かれます。
coroutineScope
内のblock
が完了した際に、未完了の子コルーチンが残っている場合:
makeCompletingOnce
でコルーチンの完了処理を進めた結果、まだ未完了の子コルーチンが存在する場合 (COMPLETING_WAITING_CHILDREN
が返された場合) は、子コルーチンの完了を待機するために、何もせずreturnします。
後ほど説明しますが、こちらの場合には、全ての子コルーチンが完了した時点でafterCompletion
が呼ばれます。
coroutineScope
内のblock
が完了した際に、未完了の子コルーチンがない場合:
makeCompletingOnce
でコルーチンが完了した場合 (= 未完了の子コルーチンが存在しない場合) には、afterResume
を呼び出します。
ScopeCoroutine
のafterResume
が呼ばれ、CoroutineScopeのスコープを抜けることとなります。
言葉だけでは難しいので、サンプルコードを図示して説明します。これはmakeCompletingOnce
の後に、未完了のコルーチンが存在するパターンに相当します。
coroutineScope {
delay(100) // suspension point: ここで実行が一時中断される。
launch { delay(1000) }
}
パターン1の流れ: coroutineScope
のblock
終了時に、未完了の子コルーチンが存在するケース
一方、以下のようなサンプルコードは、coroutineScope
のblock
終了時に未完了の子コルーチンが存在しないパターンに相当します。
coroutineScope {
launch { delay(100) }
delay(1000) // delayの間にlaunchされた子コルーチンが完了する。
}
パターン1の流れ: coroutineScope
のblock
終了時に、未完了の子コルーチンが存在しないケース
いずれのケースにおいても、「子コルーチンが完了するまで、親コルーチンが完了しない」というStructured Concurrencyの原則が満たされていることが分かります。
coroutineScope
に渡されたblock
は完了するものの、未完了の子コルーチンが存在する場合
パターン2. パターン2に相当するコードを、以下に示します。
coroutineScope {
launch { delay(1000) } // `block`内の処理が終わった直後は未完了
}
launch
自体はsuspend関数ではないのでblock
内のコードは中断されません。
しかし、block
内の処理が完了した時点では、launch
によって起動された子コルーチンが未完了です。
以下にstartUndispatched
のコードを抜粋します。
val result = try {
block.startCoroutineUninterceptedOrReturn(receiver, this)
} catch (e: DispatchException) {
dispatchExceptionAndMakeCompleting(e)
} catch (e: Throwable) {
CompletedExceptionally(e)
}
if (result === COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
val state = makeCompletingOnce(result)
if (state === COMPLETING_WAITING_CHILDREN) return COROUTINE_SUSPENDED
パターン2では、coroutineScope
内のblock
自体は、中断することなく完了します。
そのため、result
にはCOROUTINE_SUSPENDED
ではなく実行結果が入り、一つ目のif文は通過します。そして、先ほども登場したmakeCompletingOnce
が呼ばれます。
makeCompletingOnce
はコルーチンの完了処理を進めるためのメソッドです。
パターン2は、makeCompletingOnce
を実行した結果、COMPLETING_WAITING_CHILDREN
が返される、すなわち未完了の子コルーチンが残っている場合です (一方で、残っていない場合は、後述するパターン3となります)。
makeCompletingOnce
が実行されると、全ての子コルーチンが完了したタイミングで、ScopeCoroutine
に対してafterCompletion
が呼ばれます。
これによって、coroutineScope
の呼び出し元のContinuation
のresumewWith
が呼ばれ、coroutineScope
のスコープを抜けることとなります。
以下のコードを例に、パターン2における処理の流れを図示します。
coroutineScope {
launch { delay(1000) } // `block`内の処理が終わった直後は未完了
}
パターン2の処理の流れ
coroutineScope
に渡されたblock
が完了し、未完了の子コルーチンも存在しない場合
パターン3. パターン3に相当するコードを、以下に示します。
coroutineScope {
println("Hello, World!")
}
このケースは、coroutineScope
に渡されたblock
が中断せず、かつblock
の完了時点で未完了の子コルーチンも存在しないケースです。実際の使い方で、このようなケースが生じることは稀かもしれません。
このパターンでは、block
が同期的に完了し、即座にcoroutineScope
のスコープを抜けることになります。
パターン3の処理の流れ
makeCompletingOnce
の内部実装
パターン1・パターン2で使用されていた、makeCompletingOnce
の内部実装を解説します。
makeCompletingOnce
は、コルーチンの完了を進めるためのメソッドです。
makeCompletingOnce
はtryMakeCompleting
メソッドを呼び、子コルーチンが未完了な場合には、そこから更にtryMakeCompletingSlowPath
メソッドが呼ばれます。
tryMakeCompletingSlowPath
から、特に重要な部分を抜粋して以下に示します[22]。
private fun tryMakeCompletingSlowPath(state: Incomplete, proposedUpdate: Any?): Any? {
// 子コルーチンの一覧を取得する。
val list = getOrPromoteCancellingList(state) ?: return
// 子コルーチンを1つ取得する。
val child = list.nextChild()
// 未完了のコルーチンが存在する場合には、`COMPLETING_WAITING_CHILDREN`を返す。
if (child != null && tryWaitForChild(finishing, child, proposedUpdate))
return COMPLETING_WAITING_CHILDREN
return finalizeFinishingState(finishing, proposedUpdate)
}
子コルーチンに対してtryWaitForChild
メソッドを呼び出すことで、子コルーチンが完了するまで待機しています。
tryWaitForChild
のソースコードを以下に示します[23]。
private tailrec fun tryWaitForChild(state: Finishing, child: ChildHandleNode, proposedUpdate: Any?): Boolean {
val handle = child.childJob.invokeOnCompletion(
invokeImmediately = false,
handler = ChildCompletion(this, state, child, proposedUpdate)
)
// 子コルーチンが未完了であれば、returnして完了を待つ。
if (handle !== NonDisposableHandle) return true
// 他に子コルーチンがあれば、それに対して再帰的に`tryWaitForChild`を呼び出す。
val nextChild = child.nextChild() ?: return false
return tryWaitForChild(state, nextChild, proposedUpdate)
}
子コルーチンのJobに対して、invokeOnCompletion
メソッドが呼ばれています。
これはコールバック関数に等しく、子コルーチンの完了時に、ハンドラ (ChildCompletion
) が呼ばれるようになります。
また、この子コルーチン自体はすでに完了している場合には、別の子コルーチンに対してtryWaitForChild
を再帰的に呼び出しています。
これにより、未完了の子コルーチンが一つでも存在する場合には、tryWaitForChild
メソッドはtrueを返すことになります。
続いて、子コルーチンに渡されているハンドラである、ChildCompletion
のソースコードを以下に示します[24]。
private class ChildCompletion(
private val parent: JobSupport,
private val state: Finishing,
private val child: ChildHandleNode,
private val proposedUpdate: Any?
) : JobNode() {
override val onCancelling get() = false
override fun invoke(cause: Throwable?) {
parent.continueCompleting(state, child, proposedUpdate)
}
}
子コルーチンの完了時に、continueCompleting
が呼ばれることが分かります。
そこで、continueCompleting
メソッドのソースコードから、特に重要な部分を抜粋して、以下に示します。[25]
private fun continueCompleting(state: Finishing, lastChild: ChildHandleNode, proposedUpdate: Any?) {
val waitChild = lastChild.nextChild()
if (waitChild != null && tryWaitForChild(state, waitChild, proposedUpdate)) return
// 全ての子コルーチンが完了している場合、`afterCompletion`を呼び出す。
val finalState = finalizeFinishingState(state, proposedUpdate)
afterCompletion(finalState)
}
tryMakeCompletingSlowPath
と同様に、tryWaitForChild
が呼ばれています。
また、tryWaitForChild
がfalseを返した場合、すなわち全ての子コルーチンが完了している場合には、afterCompletion
が呼ばれます。
ScopeCoroutine
のafterCompletion
が呼ばれることで、coroutineScope
の呼び出し元のContinuation
に対してresumeCancellableWith
が呼ばれ、coroutineScope
のスコープから抜けることになります。
makeCompletingOnce
が呼ばれてからの流れを、図に整理します。
makeCompletingOnce
が呼ばれてからの流れ
全ての子コルーチンが完了するまで、「tryWaitForChild
-> ハンドラを子コルーチンのJobに登録 -> 子コルーチンの完了時にcontinueCompletion
が呼ばれる -> tryWaitForChild
」というループが継続することが分かります。
以上のようにして、CoroutineScopeは「全ての子コルーチンの完了を待つ」というStructured Concurrencyの原則を実現しています。
launch
がCoroutineScope
との親子構造を形成する仕組み
CoroutineScope
が、内部で起動された子コルーチンの完了を待つには、子コルーチンとの親子構造が形成される必要があります。
そこで、coroutineScope
の内部でコルーチンをlaunch
した際に、「CoroutineScope
のコルーチン」と「launch
された子コルーチン」の間に親子構造が形成される仕組みを確認します。
launch
のソースコードを以下に示します[26]。
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
Jobの親子構造の形成には、最初の2つのステップが関わっています。
newCoroutineContext
の作成StandaloneCoroutine
の作成
それぞれの実装を追っていきます。
newCoroutineContext
の作成
newCoroutineContext
はプラットフォームごとに実装が定義されています。
ここではJVM版のソースコードを示します[27]。
public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
val combined = foldCopies(coroutineContext, context, true)
val debug = if (DEBUG) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) else combined
return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null)
debug + Dispatchers.Default else debug
}
重要なのは、1行目のfoldCopies(coroutineContext, context, true)
です。
ここで、親であるCoroutineScope
の持つCoroutineContext
と、launch
のパラメータとして渡されたCoroutineContext
をマージしています。
この結果として、newCoroutineContext
が返すCoroutineContext
には、(基本的には) 親のJob
が含まれることになります。
StandaloneCoroutine
の作成
続いて、launch
内の以下の部分で、StandaloneCoroutine
あるいはLazyStandaloneCoroutine
が作られます。
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
StandaloneCoroutine
あるいはLazyStandaloneCoroutine
のいずれになるかは、コルーチンの起動直後の状態を左右します。後者は、明示的にstart
されるまでコルーチンが起動しません。
ただ、今回調査しているJob
の親子構造の形成プロセスにおいては、両者の基底クラスであるAbstractCoroutine
が関係しているため、いずれであっても違いはありません。
StandaloneCoroutine
あるいはLazyStandaloneCoroutine
の初期化パラメータとして、前ステップで作成されたnewContext
(呼び出し元のCoroutineContext
とlaunch
のパラメータのCoroutineContext
をマージしたもの) が渡されています。
これにより、基底クラスのAbstractCoroutine
へと、このCoroutineContext
が渡されることになります。
AbstractCoroutine
の初期化時の流れに関しては、既にScopeCoroutine
のパートでも触れました。再びAbstractCoroutine
のソースコードを抜粋します。
public abstract class AbstractCoroutine<in T>(
parentContext: CoroutineContext,
initParentJob: Boolean,
active: Boolean
) : JobSupport(active), Job, Continuation<T>, CoroutineScope {
init {
if (initParentJob) initParentJob(parentContext[Job])
}
init
内のinitParentJob
メソッドにより、「呼び出し元のコルーチンのJob」と「launch
されたコルーチンのJob」の間に親子構造が形成されます。
ここまでは、子コルーチンが正常に完了することを想定して、どのように親のCoroutineScope
が子コルーチンの完了を待つか、を追ってきました。
一方で、子コルーチンが完了に至らない、すなわち途中でキャンセルされるケースも存在します。
そういったケースにおいても、「子コルーチンが全て完了するまで、親コルーチンが完了しない」というStructured Concurrencyの原則は守られます。そこで次のセクションでは、キャンセル処理に関して解説します。
キャンセル処理でStructured Concurrencyが守られる仕組み
コルーチンのキャンセルには、「正常なキャンセル」と「(CancellationException
以外の) 例外発生による失敗」の2種類が存在します。
ただ、いずれのパターンでも、キャンセルの伝播に共通の機構を用いているため、併せて解説していきます。
キャンセル・例外処理の基本的な仕様
まずは簡単に、Kotlin Coroutinesにおけるキャンセル・例外処理の基本的な仕様をおさらいします。
1つ目の「正常なキャンセル」においては、あるコルーチンを正常にキャンセルすると、その子孫のコルーチンも正常にキャンセルされます。一方、親コルーチンへは、キャンセルが伝播されません。
正常なキャンセルの伝播
Job
に対してcancel
メソッドが明示的に呼ばれた場合、あるいはコルーチン内でCancellationException
が投げられた場合などに、正常なキャンセルが開始されます。
一方で、「例外発生による失敗」においては、あるコルーチンが失敗すると、その子孫のコルーチンだけでなく、その親のコルーチン (そしてその子孫のコルーチン) もキャンセルされます。
コルーチン内でCancellationException
以外の例外が投げられた場合には、失敗の伝播が開始されます。
例外発生による失敗の伝播
いずれのケースにおいても、子孫のコルーチンのキャンセルが完了してから、親コルーチンのキャンセルも完了します。
この仕様により、キャンセル時のコルーチンの生存期間も、Structured Concurrencyに従うこととなります。
例外が投げられた場合のキャンセル伝播の流れ
launch
内で例外が発生すると、launch
で起動されたコルーチンに紐づくContinuation
に対して、resumeWith(Exception)
が呼び出され、launch
が終了することとなります。
resumeWith
が呼ばれると、以下のような流れで、最終的にJobSupport
のtryMakeCompletingSlowPath
メソッドが呼ばれます。
-
resumeWith
からJobSupport.makeCompletingOnce
が呼ばれる。 -
JobSupport.makeCompletingOnce
からJobSupport.tryMakeCompleting
が呼ばれる。 -
JobSupport.tryMakeCompleting
からJobSupport.tryMakeCompletingSlowPath
が呼ばれる。
JobSupport
のtryMakeCompletingSlowPath
から、例外処理に関連するコードを抜粋して、以下に示します[28]。
private fun tryMakeCompletingSlowPath(state: Incomplete, proposedUpdate: Any?): Any? {
// `list`は子コルーチンの一覧
val list = getOrPromoteCancellingList(state) ?: return
val finishing = state as? Finishing ?: Finishing(list, false, null)
val notifyRootCause: Throwable?
synchronized(finishing) {
val wasCancelling = finishing.isCancelling
(proposedUpdate as? CompletedExceptionally)?.let { finishing.addExceptionLocked(it.cause) }
// まだキャンセル中でない場合、キャンセル処理を開始する。
// 失敗の原因となった例外を`notifyRootCause`にセットする。
notifyRootCause = finishing.rootCause.takeIf { !wasCancelling }
}
// 例外によって終了していた場合、子コルーチンにキャンセルを伝播する。
notifyRootCause?.let { notifyCancelling(list, it) }
// `tryWaitForChild`を呼び出し、全ての子コルーチンが実行完了あるいはキャンセル完了するまで待機する。
val child = list.nextChild()
if (child != null && tryWaitForChild(finishing, child, proposedUpdate))
return COMPLETING_WAITING_CHILDREN
return finalizeFinishingState(finishing, proposedUpdate)
}
tryMakeCompletingSlowPath
内で、以下のような流れで例外処理が行われます。
- 子コルーチンの一覧を
list
変数にセットする。 -
キャンセル・例外発生によって終了していた場合、その例外を
notifyRootCause
にセットする。 - キャンセル・例外発生によって終了していた場合、子コルーチンにキャンセルを伝播する。
具体的には、list
とnotifyRootCause
を引数として、notifyCancelling
が呼ばれる。 -
tryWaitForChild
メソッドにより、全ての子コルーチンが完了 (正常終了もしくはキャンセル完了) するまで待機する。
4つ目のステップで利用されているtryWaitForChild
は、子コルーチンが正常終了する際にも使われていたものです。
例外処理においても、正常系と同様の仕組みで、子コルーチンの完了が待機されていることが分かります。
続いて、notifyCancelling
メソッドのソースコード[29]を以下に示します。
private fun notifyCancelling(list: NodeList, cause: Throwable) {
// first cancel our own children
onCancelling(cause)
list.close(LIST_CANCELLATION_PERMISSION)
notifyHandlers(list, cause) { it.onCancelling }
// then cancel parent
cancelParent(cause) // tentative cancellation -- does not matter if there is no parent
}
notifyCancelling
内で行われていることとして、以下の2つが特に重要です。
-
notifyHandlers
メソッドによって、子コルーチンにキャンセルを伝播する。 -
cancelParent
メソッドによって、親コルーチンにキャンセルを伝播する。
子コルーチンへのキャンセルの伝播
notifyHandlers
メソッドのソースコード[30]を以下に示します。
private inline fun notifyHandlers(list: NodeList, cause: Throwable?, predicate: (JobNode) -> Boolean) {
var exception: Throwable? = null
list.forEach { node ->
if (node is JobNode && predicate(node)) {
try {
node.invoke(cause)
} catch (ex: Throwable) {
exception?.apply { addSuppressed(ex) } ?: run {
exception = CompletionHandlerException("Exception in completion handler $node for $this", ex)
}
}
}
}
exception?.let { handleOnCompletionException(it) }
}
node.invoke(cause)
の部分で、ChildHandleNode
のinvoke
メソッドが呼ばれています。
ChildHandleNode
のソースコード[31]を見ると、invoke
メソッドでは、ChildJob
に対してparentCancelled
が呼ばれています。
private class ChildHandleNode(
@JvmField val childJob: ChildJob
) : JobNode(), ChildHandle {
override val parent: Job get() = job
override val onCancelling: Boolean get() = true
override fun invoke(cause: Throwable?) = childJob.parentCancelled(job)
override fun childCancelled(cause: Throwable): Boolean = job.childCancelled(cause)
}
JobSupport
にあるparentCancelled
メソッドの実装を、以下に示します[32]。
public final override fun parentCancelled(parentJob: ParentJob) {
cancelImpl(parentJob)
}
cancelImpl
メソッドが呼ばれています。詳細な解説は省略しますが、cancelImpl
メソッドを通じて、再び「そのコルーチンのキャンセル」と「親・子コルーチンへのキャンセル伝播」が行われます。
このようにして、子孫コルーチンへとキャンセルが伝播していきます。
親コルーチンへのキャンセルの伝播
続いて、notifyHandlers
(子コルーチンへのキャンセル伝播) の後に呼ばれる、cancelParent
のソースコード[33]を以下に示します。
private fun cancelParent(cause: Throwable): Boolean {
if (isScopedCoroutine) return true
val isCancellation = cause is CancellationException
val parent = parentHandle
if (parent === null || parent === NonDisposableHandle) {
return isCancellation
}
return parent.childCancelled(cause) || isCancellation
}
メソッド名からも明らかなように、これは親コルーチンをキャンセルするためのメソッドです。
重要なのはparent.childCancelled
の部分で、ここで親コルーチンに対してキャンセルが伝播されます。
JobSupport
内にあるchildCancelled
のソースコードを以下に示します[34]。
public open fun childCancelled(cause: Throwable): Boolean {
if (cause is CancellationException) return true
return cancelImpl(cause) && handlesException
}
まず、原因がCancellationException
だった場合には、親のキャンセルがスキップされています。これによって「正常なキャンセルだった場合、子孫コルーチンのみに伝播する」という仕様が実現されています。
一方、原因がCancellationException
以外の例外だった場合には、親コルーチンに対して、cancelImpl
が呼び出されます。
cancelImpl
は子コルーチンへのキャンセル伝播の仕組みを追った際にも登場しました。
これは「そのコルーチンのキャンセル」と「親・子孫コルーチンへのキャンセル伝播」を行うメソッドです。
このようなメカニズムで、CancellationException
以外の例外発生時には、親コルーチンもキャンセルされることになります。
supervisorScope
を使うとなぜキャンセルが伝播しないのか
coroutineScope
の代わりにsupervisorScope
を利用すると、CancellationException
以外の例外が発生した場合でも、そのコルーチンの親・子孫に対してキャンセルが伝播しません。
supervisorScope
利用時の失敗の伝播
supervisorScope
を使ったコードの例を、以下に示します。
▶️ Kotlin Playgroundで試す
import kotlinx.coroutines.*
suspend fun main() {
supervisorScope {
launch { // Launch Coroutine A.
throw Exception("Some error message.")
}
.invokeOnCompletion { cause -> println("Completed Child Coroutine A, cause: $cause") }
launch { // Launch Coroutine B.
delay(100)
}
.invokeOnCompletion { cause -> println("Completed Child Coroutine B, cause: $cause") }
}
println("supervisorScope completed.")
}
出力:
Exception in thread "DefaultDispatcher-worker-2 @coroutine#1"
Completed Child Coroutine A, cause: java.lang.Exception: Some error message.
Completed Child Coroutine B, cause: null
supervisorScope completed.
launch
されたコルーチンで例外が発生しても、親であるsupervisorScope
や、同じsupervisorScope
内でlaunch
された他のコルーチンはキャンセルされません (試しにcoroutineScope
に変えてみると、違いがよく分かるかと思います)。
supervisorScope
のソースコード[35]から、上記の挙動となる理由が分かります。
public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return suspendCoroutineUninterceptedOrReturn { uCont ->
val coroutine = SupervisorCoroutine(uCont.context, uCont)
coroutine.startUndispatchedOrReturn(coroutine, block)
}
}
coroutineScope
と異なり、ScopeCoroutine
の代わりにSupervisorCoroutine
が起動されています。
SupervisorCoroutine
のソースコード[36]は以下の通りで、ScopeCoroutine
を継承しています。
private class SupervisorCoroutine<in T>(
context: CoroutineContext,
uCont: Continuation<T>
) : ScopeCoroutine<T>(context, uCont) {
override fun childCancelled(cause: Throwable): Boolean = false
}
SupervisorCoroutine
では、childCancelled
がオーバーライドされており、デフォルトの挙動と異なりキャンセル処理を行わない (cancelImpl
を呼ばない) ようになっています。
そのため、子コルーチンのcancelParent
メソッドからchildCancelled
が呼ばれても、supervisorScope
に紐づくコルーチンがキャンセルされません。
以上、キャンセルがどのように親・子孫コルーチンへと伝播されるのか、そしてどのように子コルーチンのキャンセル完了を待つのかを、ソースコードから明らかにしました。
なお、実際の例外処理のソースコードはもっと複雑で、説明しきれていない部分も多いことをご承知おきください。
まとめ
本記事では、Structured Concurrencyとは何か、そしてKotlin CoroutinesにおいてどのようにStructured Concurrencyが実現されているのかを解説しました。
私自身、CoroutineScopeが内部のコルーチンの完了を待つ仕組みなど、「なぜこう動くのか分からない」という部分が多くありました。
ただ、今回の調査を通じて、そういった「ブラックボックス」に光を当てたことで、より自信を持ってKotlin Coroutinesの実装やデバッグ、レビューができるようになったと思っています。
読者の皆様にも、同じような価値を提供できれば嬉しいです。また、同じくKotlin Coroutines自体のソースコードを読んでみたいという方への指針になればと思います。
-
Coroutines: https://kotlinlang.org/docs/coroutines-overview.html ↩︎
-
Kotlin Coroutinesの核心:Builder・CoroutineScope・Job・CoroutineContextの関係: https://zenn.dev/kaseken/articles/99d92a128cbc9a ↩︎
-
sustrik: https://github.com/sustrik ↩︎
-
JEP 505 Structured Concurrency: https://openjdk.org/jeps/505 ↩︎
-
libdill: https://github.com/sustrik/libdill ↩︎
-
Explanation of Structured Concurrency in libdill documentation: https://libdill.org/structured-concurrency.html ↩︎
-
njsmith: https://github.com/njsmith ↩︎
-
Notes on structured concurrency, or: Go statement considered harmful: https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/ ↩︎
-
Edsger W. Dijkstra: https://en.wikipedia.org/wiki/Edsger_W._Dijkstra ↩︎
-
構造化プログラミング: https://ja.wikipedia.org/wiki/構造化プログラミング ↩︎
-
The reason to avoid GlobalScope: https://elizarov.medium.com/the-reason-to-avoid-globalscope-835337445abc ↩︎
-
coroutineScope
のソースコード: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/CoroutineScope.kt#L280 ↩︎ -
内部実装から理解するKotlin Coroutines:suspend関数・Continuation編: https://zenn.dev/kaseken/articles/a50fd3f5e6e2ba ↩︎
-
ScopeCoroutine
のソースコード: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/internal/Scopes.kt#L11 ↩︎ -
AbstractCoroutine
のソースコード: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt#L36 ↩︎ -
JobSupport
のinitParentJob
メソッドのソースコード: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/JobSupport.kt#L141 ↩︎ -
ChildHandle
のソースコード: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/Job.kt#L461 ↩︎ -
ScopeCoroutine
のソースコード: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/internal/Scopes.kt#L11 ↩︎ -
startUndispatchedOrReturn
のソースコード: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/intrinsics/Undispatched.kt#L41 ↩︎ -
AbstractCoroutine
のresumeWith
のソースコード: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt#L98 ↩︎ -
tryMakeCompletingSlowPath
のソースコード: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/JobSupport.kt#L902 ↩︎ -
tryWaitForChild
のソースコード: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/JobSupport.kt#L954 ↩︎ -
ChildCompletion
のソースコード: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/JobSupport.kt#L1260 ↩︎ -
continueCompleting
メソッドのソースコード: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/JobSupport.kt#L965 ↩︎ -
launch
のソースコード: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/Builders.common.kt#L44 ↩︎ -
newCoroutineContext
のソースコード: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/jvm/src/CoroutineContext.kt#L14 ↩︎ -
tryMakeCompletingSlowPath
のソースコード: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/JobSupport.kt#L902 ↩︎ -
notifyCancelling
メソッドのソースコード: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/JobSupport.kt#L320 ↩︎ -
notifyHandlers
メソッドのソースコード: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/JobSupport.kt#L360 ↩︎ -
ChildHandleNode
のソースコード: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/JobSupport.kt#L1575 ↩︎ -
parentCancelled
のソースコード: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/JobSupport.kt#L667 ↩︎ -
cancelParent
のソースコード: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/JobSupport.kt#L336 ↩︎ -
childCancelled
のソースコード: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/JobSupport.kt#L680 ↩︎ -
supervisorScope
のソースコード: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/Supervisor.kt#L50 ↩︎ -
SupervisorCoroutine
のソースコード: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/Supervisor.kt#L64 ↩︎
Discussion