kotlin 协程原理分析 - 协程的线程切换

简单总结下协程线程切换的原理:

0 前言

协程线程切换的原理很简单,如果能理解微模型,那么协程切换实际上就是不同微笑模型之前的切换;

1 简单场景

1
2
3
4
5
6
7
8
fun main() {
GlobalScope.launch {
println("GlobalScope 协程开始执行")
withContext(Dispatchers.IO) {
println("线程切换执行完毕!")
}
}
}

考虑上面这样一个场景

GlobalScope 启动的协程在内部通过 withContext 执行了线程切换,那么是如何切换的呢?

2 withContext 原理分析

withContext 是一个挂起函数,不熟悉挂起函数原理的可以先去看看

通过微模型我们知道,协程启动三要素:

  • 关联任务 job,用于协程之间的协同。
  • 分发器 Dispatchers:协程在特定线程池中运行。
  • 协程体 SuspendLamda:协程体逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
// compute new context
val oldContext = uCont.context
// Copy CopyableThreadContextElement if necessary
val newContext = oldContext.newCoroutineContext(context)
// always check for cancellation of new context
newContext.ensureActive()
// FAST PATH #1 -- new context is the same as the old one
if (newContext === oldContext) {
val coroutine = ScopeCoroutine(newContext, uCont)
return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
}
//【1】如果没有指定分发起,就在父协程的 dispatcher 中分发。
if (newContext[ContinuationInterceptor] == oldContext[ContinuationInterceptor]) {
val coroutine = UndispatchedCoroutine(newContext, uCont)
// There are changes in the context, so this thread needs to be updated
withCoroutineContext(newContext, null) {
return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
}
}
//【2】切换线程
val coroutine = DispatchedCoroutine(newContext, uCont)
block.startCoroutineCancellable(coroutine, coroutine)
coroutine.getResult()
}
}

对于 launch 启动的协程:

  • 关联任务 job:StandardConroutine。
  • 分发器 Dispatchers:Dispatchers.MAIN。
  • 协程体 SuspendLamda:协程体

那么对于 withContext:

  • 关联任务 job:如果指定了不一样的分发器;DispatchedCoroutine,如果和父亲协程一样,就是 UnDispatchedCoroutine。
  • 分发器 Dispatchers:如果指定了就是 Dispatchers.IO,没指定就是父亲协程的 Dispatchers.IO
  • 协程体 SuspendLambda:协程体

这样看 withContext 也是执行了启动协程的微模型的操作!!

3 从微模型看协程的线程切换

在微模型里,协程体的执行主要是如下流程:

  • 协程体 SuspendLambda 被包裹成一个 DispatchedContinuation 对象;
  • DispatchedContinuation 被丢到 Dispatchers 线程池中运行;
  • DispatchedContinuation 执行 协程体 SuspendLambda 的 resumeWith 方法;
  • resumeWith 方法会触发协程体 SuspendLambda 的 invokeSuspend 方法,执行协程业务代码;
  • 执行完成后,通过 completion.resumeWith 通知 Job 协程执行完成了!

launch 和 withContext 其实都是做的这个操作。

那么他们的切换是基于什么操作呢?实际上,关键点就是在关联的 Job 上:DispatchedCoroutine。

下面进一步分析。

4 通过 withContext 切换到 IO 线程

这个很好理解 withContext 是一个 suspend 函数,同时指定了不同的分发器,那么 withContext 启动的 DispatchedCoroutine 的状态默认 UNDECIDE。

那么此时 coroutine.getResult() 返回一定是 SUSPEND。

从而导致外面的 launch 协程挂起等待了。

这个没什么好说的。

5 withContext 切换回 launch 所在的 MAIN 线程

关键点在于 DispatchedCoroutine。

协程体执行完成后,会通过 completion.resumeWith 通知 Job 协程执行完成了!

withContext 对应的 job 就是 DispatchedCoroutine,我们去看看 DispatchedCoroutine:

DispatchedCoroutine

构造器逻辑,里面持有外部写成的 Continuation 对象:

1
2
3
4
internal class DispatchedCoroutine<in T>(
context: CoroutineContext,
uCont: Continuation<T> //【1】持有外部协程的 DispatchedContinuation 对象。
) : ScopeCoroutine<T>(context, uCont) {

resumeWith –> afterResume

我们来看下 afterResume 在 DispatchedCoroutine 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// AbstractCoroutine
public final override fun resumeWith(result: Result<T>) {
val state = makeCompletingOnce(result.toState())
if (state === COMPLETING_WAITING_CHILDREN) return
afterResume(state)
}

protected open fun afterResume(state: Any?): Unit = afterCompletion(state)



// DispatchedCoroutine
override fun afterCompletion(state: Any?) {
// Call afterResume from afterCompletion and not vice-versa, because stack-size is more
// important for afterResume implementation
afterResume(state)
}

override fun afterResume(state: Any?) {
if (tryResume()) return // completed before getResult invocation -- bail out
// Resume in a cancellable way because we have to switch back to the original dispatcher
uCont.intercepted().resumeCancellableWith(recoverResult(state, uCont))
}

关键点就在 afterResume 里面。

他获取到 launch 协程的 Continuation 对象,调用 intercepted() 方法,获取到其对应的 DispatchedContinuation 对象,再次 resumeCancellableWith

这个很熟悉了,其实就是把 launch 协程又唤醒了,并且 DispatchedContinuation 内部支持在特定的线程池中执行协程逻辑。

其他的切换线程的方式大体也是这样。

6 普通 suspend 函数如何切换线程

普通 suspend 函数这里特指我们自己业务中定义的 suspend 函数。

普通 suspend 函数切换线程的逻辑在业务 block 里面,对于 suspend 函数经过编译器和内部框架产生的代码,这部分所在的线程 Dispatchers 复用的外部协程的:

suspendCancellableCoroutine

这里可以看到 cancellable.getResult() 是在调用 suspend 函数的线程中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public suspend inline fun <T> suspendCancellableCoroutine(
crossinline block: (CancellableContinuation<T>) -> Unit
): T =
//【1】执行校验,不重要;
//【2】这里的 uCont 对象就是前面的 $completion
suspendCoroutineUninterceptedOrReturn { uCont ->
//【2】创建了 CancellableContinuationImpl 对象,包裹外部的 $completion 的 DispatchedContinuation 对象;
val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE)
/*
* For non-atomic cancellation we setup parent-child relationship immediately
* in case when `block` blocks the current thread (e.g. Rx2 with trampoline scheduler), but
* properly supports cancellation.
*/
cancellable.initCancellability()
//【3】执行我们的逻辑,并传入 CancellableContinuationImpl;
block(cancellable)
//【4】立刻返回结果,如果是自线程的话,返回的是 suspend 状态;
cancellable.getResult()
}

resumeWith –> dispatchResume

dispatchResume 最终会调用到 dispatch 方法里面,可以看到 suspend 函数的情况下,利用的是调用 suspend 函数的协程所在的 Dispatchers。

这样确保业务环境没问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
internal fun <T> DispatchedTask<T>.dispatch(mode: Int) {
assert { mode != MODE_UNINITIALIZED } // invalid mode value for this method
val delegate = this.delegate
val undispatched = mode == MODE_UNDISPATCHED
if (!undispatched && delegate is DispatchedContinuation<*>
&& mode.isCancellableMode == resumeMode.isCancellableMode) {
//【1】这里是外部协程的 DispatchedContinuation
val dispatcher = delegate.dispatcher
val context = delegate.context
//【2】这里就是在外部协程的 Dispatchers 线程里面分发自己;
if (dispatcher.isDispatchNeeded(context)) {
dispatcher.dispatch(context, this)
} else {
resumeUnconfined()
}
} else {
// delegate is coming from 3rd-party interceptor implementation (and does not support cancellation)
// or undispatched mode was requested
resume(delegate, undispatched)
}
}

7 总结

可以看到协程线程切换的原理,也是基于微模型:

  • 1、通过 job 关联实现协程的恢复操作;
  • 2、通过 DispatchedTask 的统一分发和调用实现线程的切换;
  • 3、普通的 suspend 函数依然复用调用者的线程池;
文章作者: Li Shuaiqi
文章链接: https://lishuaiqi.top/2025/05/12/kotlin/kotlin-cotoutine-coroutine_switch/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Li Shuaiqi's Blog