Open6

Kotlin Coroutines の launch のコードリーディング

watabeewatabee
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
}

launch 関数は上記のようになっている。
やっていることは以下。

  • 新しい CoroutineContext を作成
  • コルーチンのインスタンスを作成し、開始する
    • startCoroutineStart.Lazy の場合 LazyStandaloneCoroutine、そうでない場合は StandaloneCoroutine のインスタンスを作る
  • 戻り値の Job のインスタンスとして作成したコルーチンのインスタンスを返す
watabeewatabee

以下 StandaloneCoroutine の場合について見ていく。
StandaloneCoroutine の定義は以下。

private open class StandaloneCoroutine(
    parentContext: CoroutineContext,
    active: Boolean
) : AbstractCoroutine<Unit>(parentContext, initParentJob = true, active = active) {
    ...
}
watabeewatabee

AbstractCoroutine の定義は以下。

public abstract class AbstractCoroutine<in T>(
    parentContext: CoroutineContext,
    initParentJob: Boolean,
    active: Boolean
) : JobSupport(active), Job, Continuation<T>, CoroutineScope {
  
    ...

    public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) {
        start(block, receiver, this)
    }
}

JobCoroutineScope を継承している。

launch 関数の coroutine.start(start, coroutine, block) の部分は上記の関数が呼ばれている。
このシグネチャから RStandaloneCoroutine なので、block の型は StandaloneCoroutine.() -> T となる。

launch 関数の第三引数の block の型は CoroutineScope.() -> Unit なので block のレシーバーは StandaloneCoroutine のインスタンスになっている。

ここから launch でコルーチンを開始した場合に以下のことが言える。

  1. launch を呼び出すとコルーチンのインスタンスが作成される
  2. 返り値の Job のインスタンスは 1 で作成したコルーチンのインスタンスと同じ
  3. launch の第三引数に指定するラムダのレシーバーである CoroutineScope は 1 で作成したコルーチンのインスタンスとなる
watabeewatabee

AbstractCoroutinestart 関数の処理を見る。
CoroutineStartinvoke メソッドが呼ばれている。

@InternalCoroutinesApi
public operator fun <R, T> invoke(block: suspend R.() -> T, receiver: R, completion: Continuation<T>): Unit =
    when (this) {
        DEFAULT -> block.startCoroutineCancellable(receiver, completion)
        ATOMIC -> block.startCoroutine(receiver, completion)
        UNDISPATCHED -> block.startCoroutineUndispatched(receiver, completion)
        LAZY -> Unit // will start lazily
    }

CoroutineStart の値によって呼び出される処理が変わっていて、ここでコルーチンが開始される模様。
LAZY の場合は明示的に start を呼び出さないと開始されないので、ここでは開始されないようになっている。

watabeewatabee

CoroutineStart.DEFAULT の場合に呼ばれる block.startCoroutineCancellable(receiver, completion) は以下のようになっている。

internal fun <R, T> (suspend (R) -> T).startCoroutineCancellable(
    receiver: R, completion: Continuation<T>,
    onCancellation: ((cause: Throwable) -> Unit)? = null
) =
    runSafely(completion) {
        createCoroutineUnintercepted(receiver, completion).intercepted().resumeCancellableWith(Result.success(Unit), onCancellation)
    }

createCoroutineUnintercepted() はマルチプラットフォームの実装になっていて JVM の実装は以下。

https://github.com/JetBrains/kotlin/blob/v1.8.10/libraries/stdlib/jvm/src/kotlin/coroutines/intrinsics/IntrinsicsJvm.kt#L118-L130

createCoroutineFromSuspendFunction は以下のような実装になっている。

https://github.com/JetBrains/kotlin/blob/v1.8.10/libraries/stdlib/jvm/src/kotlin/coroutines/intrinsics/IntrinsicsJvm.kt#L160-L203

メソッド名が suspend 関数からコルーチンを作成する関数となっている。
Continuation のインスタンスが返されているので、suspend 関数から Continuation を作成して JVM 上で実行できる形にしているような気がする。

watabeewatabee

ちなみに Job が子を持つようになっている。

https://github.com/Kotlin/kotlinx.coroutines/blob/1.6.4/kotlinx-coroutines-core/common/src/Job.kt#L199-L216

なのでコルーチンの階層構造は実際には Job の階層構造と言えそう。

val scope = CoroutineScope(Job())
scope.launch {
    ...
}

scope.launch {
}

例えば上記のようなコードの場合は、以下のような関係性になっている。

                   Job (= ルートのCoroutineScope が持つ Job)
                      /                   \ 
Job (= StandaloneCoroutine)          Job (= StandaloneCoroutine)