按调用栈分类
由于协程需要支持挂起、恢复,因此对于挂起点的状态保存就显得极其关键。类似地,线程会因为 CPU 调度权的切换而被中断,它的中断状态会保存在调用栈当中,因而协程的实现也按照是否开辟相应的调用栈存在以下两种类型:
-
有栈协程 Stackful Coroutine:每一个协程都会有自己的调用栈,有点儿类似于线程的调用栈,这种情况下的协程实现其实很大程度上接近线程,主要不同体现在调度上。
-
无栈协程 Stackless Coroutine:协程没有自己的调用栈,挂起点的状态通过状态机或者闭包等语法来实现。
有栈协程的优点就是可以在任意函数调用层级的任意位置进行挂起,并转移调度权,例如 Lua 的协程,这方面多数无栈协程就显得力不从心了,例如 Python 的 Generator;通常来讲,有栈协程因为总是会给协程开辟一块儿栈内存,因此内存开销也相对可观,而无栈协程在内存方面就比较有优势了。
Kotlin 的协程通常被认为是一种无栈协程的实现,它的控制流转依靠对协程体本身编译生成的状态机的状态流转来实现,变量保存也是通过闭包语法来实现的。不过,Kotlin 的协程可以在挂起函数范围内的任意调用层次挂起,换句话说,我们启动一个 Kotlin 协程,可以在其中任意嵌套 suspend 函数,而这又恰恰是有栈协程最重要的特性之一。
嵌套suspend函数示例:
suspend fun level_0() {
println("I'm in level 0!")
level_1() // ............ ①
}
suspend fun level_1() {
println("I'm in level 1!")
suspendNow() // ............ ②
}
suspend fun suspendNow() = suspendCoroutine<Unit> { ... }
示例中 ① 处并没有真正直接挂起,② 处的调用才会真正挂起,Kotlin 通过 suspend 函数嵌套调用的方式可以实现任意函数调用层次的挂起。
当然,想要在任意位置挂起,那就需要调用栈了,与开发者通过调用 API 显式地挂起协程相比,任意位置的挂起主要用于运行时对协程执行的干预,这种挂起方式对于开发者不可见,因而是一种隐式的挂起操作。
按调度方式分类
调度过程中,根据协程转移调度权的目标又将协程分为对称协程和非对称协程:
-
对称协程 Symmetric Coroutine:任何一个协程都是相互独立且平等的,调度权可以在任意协程之间转移。
-
非对称协程 Asymmetric Coroutine:协程出让调度权的目标只能是它的调用者,即协程之间存在调用和被调用关系。
对称协程实际上已经非常接近线程的样子了,而非对称协程的调用关系实际上也更符合我们的思维方式,常见的语言对协程的实现大多是非对称实现,例如 async/await,await 时将调度权转移到异步调用中,异步调用返回结果或抛出异常时总是将调度权转移回 await 的位置。
从实现的角度来讲,非对称协程的实现更自然,也相对容易;不过,我们只要对非对称协程稍作修改,即可实现对称协程的能力。在非对称协程的基础上,我们只需要添加一个中立的第三方作为协程调度权的分发中心,所有的协程在挂起时都将控制权转移给分发中心,分发中心根据参数来决定将调度权转移给哪个协程,例如 Kotlin 协程框架中基于
Channel 的通信。
协程与线程的区别
-
线程是属于 操作系统的概念,而 协程则属于 编程语言的范畴,它属于应用程序层的Api层的东西,可以运行在线程框架之上,由线程框架在背后操作代码的调度。
-
协程本身的概念实际包含了线程调度的概念,只有能 控制线程切换,才有可能实现 真正的异步功能(不阻塞当前线程)。
Kotlin中协程框架的分类
参考:《深入理解Kotlin协程》- 2020年-机械工业出版社-霍丙乾