一个coroutine创建好之后,就交给协程框架去调度了。这篇主要讲从launch{...}开始,到最终得到执行的时候,所涉及到的协程框架内部概念。
一般开发中所接触到的协程类和接口无非是 launch, async, Dispatch.IO...,这些概念是对我们开发者来说的。进入协程源码的世界之后,这些概念就会被一些内部概念所替代。搞清楚内部概念对分析协程源码来说非常关键。
协程的最小粒度-Coroutine
对没接触过协程的人来说,一个OOP代码的最小调度粒度是函数。在协程中,最小的调度粒度是协程,在kotlin中叫coroutine。
下面是一段协程代码,
代码语言:javascript复制fun main() = runBlocking{
launch{
launch {
// 在后台启动一个新的协程并继续r
println("launch 3 > Thread: ${Thread.currentThread().name}")
delay(1000L) // 非阻塞的等待 1 秒钟(默认时间单位是毫秒)
println("World!") // 在延迟后打印输出
}
println("launch 2 > Thread: ${Thread.currentThread().name}")
delay(100L)
println("Hello,") // 协程已在等待时主线程还在继续
Thread.sleep(200L) // 阻塞主线程 2 秒钟来保证 JVM 存活
}
println("out launch done")
}
上面的代码中总共有三个coroutine。上面的代码的执行结果是,
out launch done launch 2 > Thread: main launch 3 > Thread: main Hello, World!
每个协程实际上是个调度单位。上面的执行顺序很有意思,在 "out launch done"结束之后,首先打印了"launch 2"。按理说接下来应该是“Hello”,但实际情况是“launch 3”。
因为协程在遇到挂起函数delay的时候,会把当前coroutine挂起,然后调度另外一个待执行的coroutine去执行。
在“launch 2”之后,协程遇到了另外一个delay,此时这个coroutine又会被挂起,然后切入之前那个coroutine。所以会看到“launch 2”之后不是“launch 3”,而是Hello。
这个例子是为了说明coroutine的调度原理。从这个角度看,可以理解“用同步代码写异步逻辑”的意思。对于开发者来说,上面的代码是按同步的思路写的。实际运行中,因为coroutine的调度,则变成了异步代码。
外部概念和内部概念
协程中外部概念和内部概念的差别很大。对应开发者来说,一个协程的最小粒度coroutine,在协程的内部概念中叫DispatchedContinuation。
Continuation是一个内部接口,它有一个对象和一个方法,
代码语言:javascript复制public interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(result: Result<T>)
}
这个接口的含义是,实现这个接口的类,具有可以暂停和继续的能力,也就是resumeWith()
方法。
在协程源码里,所有的coroutine都会被封装成 DispatchedContinuation对象,但它实际上还是一个coroutine,只是增加了一些能力,这个我们后面会说。
另外还有很多内部概念,比较重要的是 Dispatcher 和 Scheduler。
Dispatcher
顾名思义这是个分发器类。它负责把 DispatchedContinuation 分发到不同的调度器中去执行。
Dispatcher的默认实现是一个 expect 类,
代码语言:javascript复制public expect object Dispatchers {
public val Default: CoroutineDispatcher
public val Main: MainCoroutineDispatcher
public val Unconfined: CoroutineDispatcher
}
它提供了几个默认的派发逻辑,比如Default会在默认线程池中随机执行coroutine,Main会在主线程,比如Android中的UI线程里执行coroutine。还有Unconfined。
在jvm上,还多了个IO类型的默认实现。对于磁盘IO或者网络IO,一般用这个作为默认的实现。它对线程池有特殊的调度方式,可以保证计算密集型的coroutine效率不会受到IO coroutine的拖累。
Scheduler
调度器是协程的核心功能,所有的corotuine最后都在Scheduler中执行。
在默认情况下调度器的实现是 CoroutineScheduler,它会根据当前任务数量创建线程,把coroutine放到线程的自有队列和公有队列中。并且还实现了 ForkJoinPool 的核心思想 work-stealing。
代码语言:javascript复制private fun trySteal(blockingOnly: Boolean): Task? {
...
}
work-stealing的核心逻辑是,所有线程先去执行CPU密集型的coroutine。CPU密集型队列执行完之后,线程再去执行IO密集型coroutine。
最精彩的部分来了,当线程A的IO密集队列执行完毕,队列为空之后,它会去偷其他线程的IO任务队列。这是整个协程调度里最精彩的部分,work-stealing的设计,加上把CPU-bounded和IO-intensive任务区分出来,使得用了协程的代码效率得到极大的提升。
为什么可以提升效率,在Kotlin协程-协程设计的基础中有具体解释。
协程框架三大件,Continuation-Disptacher-Scheduler
Kotlin的协程从框架设计上就考虑了跨平台的问题。
这里的跨平台不是指安卓和服务端。而是指kotlin在支持 jvm / js/ native 三个平台上的跨平台。从它的设计上也能感受到kotlin想吊打其他语言的野心。俗话说“javascript抢别人的活”,现在看kotlin连js的活也想抢。不知道过个十年kotlin和js有哪个还能被开发者使用。
kotlinx-coroutine的代码框架这么分,
common --jvm --js --native
在kotlinx-coroutines-core中有个公有的 common 包。除此之外还有js,jvm,native三个包。
协程框架三大件在common里有通用实现,具体到每个平台上,还有真正的细节实现。
比如Dispatcher,在 common 包中是 Dispatchers.common.kt,
代码语言:javascript复制public expect object Dispatchers {
public val Default: CoroutineDispatcher
public val Main: MainCoroutineDispatcher
public val Unconfined: CoroutineDispatcher
}
在jvm包中,是Dispatchers
代码语言:javascript复制public expect object Dispatchers {
public actual val Default: CoroutineDispatcher = createDefaultDispatcher()
...
}
可以看到jvm包中的方法名多了 actual修饰,后面也有具体的实现createDefaultDispatcher()。这是在java平台上真正用到的代码。