Kotlin 协程源码解析:DispatchedContinuation 里的 Dispatcher 从哪里来?

0 阅读3分钟

Kotlin 协程源码解析:DispatchedContinuation 里的 Dispatcher 从哪里来?

之前研究协程恢复的时候,我们已经知道了一个很重要的对象:DispatchedContinuation。

当协程需要切换线程恢复时,它会参与后面的调度流程,最终进入 CoroutineScheduler,再由线程执行 DispatchedContinuation.run()。

但当时还留下了一个问题:

DispatchedContinuation 是什么时候创建出来的?

尤其是它里面的 dispatcher:

DispatchedContinuation(
    dispatcher,
    continuation
)

这个 dispatcher 到底是从哪里来的?

这次我们就从 launch(Dispatchers.IO) 开始,顺着源码往回追。

从 launch 开始

假设我们有这样一段代码:

launch(Dispatchers.IO) {
    println("hello")
}

launch 最开始会创建一个 StandaloneCoroutine:

val coroutine = StandaloneCoroutine(
    newContext,
    active = true
)

这里的 newContext 就包含了我们传入的 Dispatchers.IO。

随后:

coroutine.start(start, coroutine, block)

默认情况下,CoroutineStart 是 DEFAULT,最终会进入:

block.startCoroutineCancellable(receiver, completion)

继续往下:

createCoroutineUnintercepted(receiver, completion)
    .intercepted()
    .resumeCancellableWithInternal(Result.success(Unit))

真正有意思的地方出现了:

.intercepted()

所以接下来我们只追这个 intercepted()。

先创建一个 Continuation

createCoroutineUnintercepted() 最终会走到 JVM 的实现。

对于普通的 suspend lambda,它会调用:

createCoroutineFromSuspendFunction(
    completion
) {
    ...
}

这里有一段非常关键的代码:

val context = completion.context

return object : ContinuationImpl(
    completion as Continuation<Any?>,
    context
) {
    ...
}

也就是说,协程真正开始执行之前,首先创建了一个 ContinuationImpl。

而这个 ContinuationImpl 的 context:

ContinuationImpl.context

来自:

completion.context

那么这个 completion 是谁?

回到前面:

StandaloneCoroutine(newContext, active = true)

也就是说,completion 本身就是我们刚刚创建的协程对象,它持有的 CoroutineContext 中已经包含了:

Dispatchers.IO

因此,到这里可以画出一条很重要的关系:

launch(Dispatchers.IO)
        ↓
StandaloneCoroutine
        ↓
completion.context
        ↓
ContinuationImpl.context

Dispatcher 并不是在 intercepted() 里面才被放进去的。

它在更早的时候,就已经存在于这个 Continuation 的 CoroutineContext 中了。

intercepted() 做了什么?

接下来就是我们真正要找的代码:

public fun intercepted(): Continuation<Any?> =
    _intercepted
        ?: (context[ContinuationInterceptor]
            ?.interceptContinuation(this) ?: this)
            .also { _intercepted = it }

这里其实做了两件事。

第一件:

context[ContinuationInterceptor]

从当前 Continuation 的 CoroutineContext 中找到 ContinuationInterceptor。

我们不需要在这里深入 CombinedContext 的链表结构,也不需要研究 Key 的具体实现,只需要知道:

ContinuationInterceptor.Key

可以从 Context 中找到对应的 Element。

而 CoroutineDispatcher 本身就是 ContinuationInterceptor。

因此,对于:

launch(Dispatchers.IO)

这里最终拿到的就是对应的 Dispatchers.IO。

于是代码实际上可以近似理解成:

Dispatchers.IO.interceptContinuation(this)

真正的关键就在这里。

Dispatcher 创建了 DispatchedContinuation

CoroutineDispatcher 对 ContinuationInterceptor 的实现是:

public final override fun <T> interceptContinuation(
    continuation: Continuation<T>
): Continuation<T> =
    DispatchedContinuation(this, continuation)

终于找到答案了。

这里的:

this

就是当前的 CoroutineDispatcher。

而第二个参数:

continuation

就是刚才创建出来的 ContinuationImpl。

所以:

DispatchedContinuation(this, continuation)

实际上就是:

DispatchedContinuation(
    dispatcher = Dispatchers.IO,
    continuation = ContinuationImpl
)

这也解释了一个之前容易产生误解的地方:

DispatchedContinuation 并不是自己去寻找 Dispatcher。

真正的过程是:

ContinuationImpl
      │
      │ intercepted()
      ↓
CoroutineContext
      │
      │ [ContinuationInterceptor]
      ↓
Dispatchers.IO
      │
      │ interceptContinuation(this)
      ↓
DispatchedContinuation

Dispatcher 作为 ContinuationInterceptor 存在于 CoroutineContext 中。

当 ContinuationImpl 被 intercepted() 时,它从 Context 中找到这个 Dispatcher,然后把自己交给 Dispatcher:

interceptContinuation(this)

最后由 Dispatcher 创建出:

DispatchedContinuation

这和我们之前研究的恢复流程接起来了

到这里,我们其实把之前研究过的两段源码接起来了。

前面我们研究过:

协程恢复
    ↓
DispatchedContinuation
    ↓
dispatch
    ↓
CoroutineScheduler
    ↓
TaskImpl
    ↓
DispatchedContinuation.run()

这次我们补上了 DispatchedContinuation 的出生过程:

launch(Dispatchers.IO)
        ↓
CoroutineContext
        ↓
ContinuationImpl
        ↓
intercepted()
        ↓
找到 Dispatchers.IO
        ↓
interceptContinuation()
        ↓
DispatchedContinuation
        ↓
        ...
        ↓
CoroutineScheduler
        ↓
DispatchedContinuation.run()

这样一来,之前看起来有些零散的源码终于连成了一条线。

我们之前知道的是:

协程恢复以后,Dispatcher 最终会把任务交给线程调度器。

现在又知道了另一半:

这个 Dispatcher 其实早就存在于 CoroutineContext 中,在协程启动时通过 intercepted() 被取出来,并参与创建 DispatchedContinuation 。

所以 DispatchedContinuation 可以看成是连接两边的一个关键对象:

CoroutineContext
      │
      │ 提供 Dispatcher
      ↓
DispatchedContinuation
      │
      │ 负责后续恢复/调度
      ↓
CoroutineScheduler

到这里,我们关于“Dispatcher 是怎么进入 DispatchedContinuation 的”这个问题,也就算真正闭环了。