简单总结下协程线程切换的原理:
0 前言
协程线程切换的原理很简单,如果能理解微模型,那么协程切换实际上就是不同微笑模型之前的切换;
1 简单场景
1 | fun main() { |
考虑上面这样一个场景
GlobalScope 启动的协程在内部通过 withContext 执行了线程切换,那么是如何切换的呢?
2 withContext 原理分析
withContext 是一个挂起函数,不熟悉挂起函数原理的可以先去看看
通过微模型我们知道,协程启动三要素:
- 关联任务 job,用于协程之间的协同。
- 分发器 Dispatchers:协程在特定线程池中运行。
- 协程体 SuspendLamda:协程体逻辑
1 | public suspend fun <T> withContext( |
对于 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 | internal class DispatchedCoroutine<in T>( |
resumeWith –> afterResume
我们来看下 afterResume 在 DispatchedCoroutine 的实现:
1 | // AbstractCoroutine |
关键点就在 afterResume 里面。
他获取到 launch 协程的 Continuation 对象,调用 intercepted() 方法,获取到其对应的 DispatchedContinuation 对象,再次 resumeCancellableWith
这个很熟悉了,其实就是把 launch 协程又唤醒了,并且 DispatchedContinuation 内部支持在特定的线程池中执行协程逻辑。
其他的切换线程的方式大体也是这样。
6 普通 suspend 函数如何切换线程
普通 suspend 函数这里特指我们自己业务中定义的 suspend 函数。
普通 suspend 函数切换线程的逻辑在业务 block 里面,对于 suspend 函数经过编译器和内部框架产生的代码,这部分所在的线程 Dispatchers 复用的外部协程的:
suspendCancellableCoroutine
这里可以看到 cancellable.getResult() 是在调用 suspend 函数的线程中:
1 | public suspend inline fun <T> suspendCancellableCoroutine( |
resumeWith –> dispatchResume
dispatchResume 最终会调用到 dispatch 方法里面,可以看到 suspend 函数的情况下,利用的是调用 suspend 函数的协程所在的 Dispatchers。
这样确保业务环境没问题。
1 | internal fun <T> DispatchedTask<T>.dispatch(mode: Int) { |
7 总结
可以看到协程线程切换的原理,也是基于微模型:
- 1、通过 job 关联实现协程的恢复操作;
- 2、通过 DispatchedTask 的统一分发和调用实现线程的切换;
- 3、普通的 suspend 函数依然复用调用者的线程池;