前言
本文将使用 Kotlin 协程的基础设施来构建常见的复合协程,带你了解仅包含了基本的挂起(suspendCoroutine)和恢复(resume)的简单协程该怎么使用。在使用复杂的官方协程框架之前,理解这些底层的状态流转非常重要。
序列生成器
序列生成器包含了"序列"和"生成器"两部分,对于我们框架设计者来说,"生成器"的实现才是关键。接下来,我们就通过仿写 Python 的 Generator 来熟悉协程的简单用法。
这个场景中,要特别注意一点:生成器是一个单向抛出数据的模型。
仿 Python 的 Generator 实现
Python 中的实现效果:在任意函数中可通过 yield 函数将当前函数挂起,并将调用 yield 时的参数作为迭代器的下一个元素。
在 Kotlin 中的实现效果为:通过 generator 可以获得一个 numbers 函数,调用这个函数即可得到一个序列生成器。序列的元素由 yield 函数的参数来传递,该函数是挂起函数,调用会立即挂起(并将数据"抛"给外部)。在序列生成器尝试获取下一个元素时恢复执行。
kotlin
val numbers = generator { start: Int -> // 该参数由我们指定,作为生成器的"种子",可为任意类型
// 生成逻辑
for (i in 0..2) {
yield(start + i)
}
}
val generator = numbers(5)
for (num in generator) {
println(num)
}
上述代码的执行流程如时序图所示:
主动挂起 Main-->>Generator: 切换执行权
【挂起主协程】 %% 第二步:生成器运行 -> yield(0) 挂起 Generator->>Generator: 转为 Running(运行) Note over Generator: 执行 yield(0)
主动挂起,并抛出 0 Generator-->>Main: 切换执行权
【恢复主协程】 %% 第三步:主协程恢复 -> loop@1 挂起 Main->>Main: 恢复 Running(运行) Note over Main: 执行 loop@1
主动挂起 Main-->>Generator: 切换执行权
【挂起主协程】 %% 第四步:生成器恢复 -> yield(1) 挂起 Generator->>Generator: 恢复 Running(运行) Note over Generator: 执行 yield(1)
主动挂起,并抛出 1 Generator-->>Main: 切换执行权
【恢复主协程】 %% 最终状态 Note over Main: 最终 Running(运行) Note over Generator: 最终 Suspended(挂起)
根据上述代码,我们可以定义出 Generator 接口、generator 函数,和函数中的作用域 GeneratorScope(提供 yield 函数):
kotlin
// 生成器接口
interface Generator<T> {
// 重载迭代器运算符,以便使用 for-in 循环遍历元素
operator fun iterator(): Iterator<T>
}
// 生成器作用域
@RestrictsSuspension
interface GeneratorScope<T> {
suspend fun yield(value: T)
}
// 返回值类型为函数,用于创建生成器
fun <T> generator(block: suspend GeneratorScope<T>.(T) -> Unit): (T) -> Generator<T> {
return { parameter: T ->
// TODO: 后续提供
GeneratorImpl(block, parameter)
}
}
Generator 中包含着迭代器,调用迭代器的 hasNext 或 next 都会触发下一个元素的获取,挂起的逻辑自然属于该迭代器的内部状态:
kotlin
// 迭代器状态
sealed class State {
class NotReady(val continuation: Continuation<Unit>) : State()
class Ready<T>(val continuation: Continuation<Unit>, val nextValue: T) : State()
object Done : State()
}
该迭代器的状态有三种:
- NotReady: 下一个元素尚未准备好,通常是挂起后还未恢复执行的情况。
- Ready: 生成器内部调用
yield产生了新元素并挂起时,会进入该状态,等待外部取走数据。 - Done: 生成器执行完毕,后续将无新元素产生。
这三种状态的流转图:
该迭代器的基本定义如下:
kotlin
// 迭代器
class GeneratorIterator<T>( // T 为元素类型
private val block: suspend GeneratorScope<T>.(T) -> Unit,
private val parameter: T
) : Iterator<T> {
private var state: State
init {
val coroutineBlock: suspend GeneratorScope<T>.() -> Unit = {
block(parameter)
}
val start = coroutineBlock.createCoroutine(
receiver = object : GeneratorScope<T> {
override suspend fun yield(value: T) {
TODO("Not yet implemented")
}
},
completion = object : Continuation<Any?> {
override val context: CoroutineContext
get() = EmptyCoroutineContext
override fun resumeWith(result: Result<Any?>) {
TODO("Not yet implemented")
}
}
)
state = State.NotReady(start)
}
// 省略 next, hasNext 待具体实现...
}
接下来实现 yield,代码如下:
kotlin
// 协程内部会调用 yield,将 value 丢给外部,自己挂起等待恢复
override suspend fun yield(value: T) = suspendCoroutine { continuation ->
state = when (state) {
is State.NotReady -> {
// 将当前的挂起点保存,并将要抛出的数据也存进状态里
State.Ready(continuation, value)
}
is State.Ready<*> -> {
throw IllegalArgumentException("Cannot yield while ready")
}
State.Done -> {
throw IllegalArgumentException("Cannot yield while done")
}
}
}
状态转移要考虑原子性,但本例中的生成器仅处于单线程环境中,故无需进行并发设计。
yield 用于产生新元素,并且挂起生成器 ,因此它一定是一个挂起函数。为了后续能够恢复执行,我们将当前挂起点的 Continuation 实例获取并保存到了迭代器内部状态中,并将状态设置为 Ready。
同理,我们完成恢复、完成等事件的状态流转:
kotlin
private fun resume() {
when (val currentState = state) {
// 外部调用 resume 时,通过存起来的挂起点将之前挂起的生成器恢复
is State.NotReady -> currentState.continuation.resume(Unit)
is State.Ready<*>, State.Done -> {}
}
}
@Suppress("UNCHECKED_CAST")
override fun next(): T {
return when (val currentState = state) {
State.Done -> {
throw IndexOutOfBoundsException("No value left.")
}
is State.NotReady -> {
// 恢复执行并多次调用 next(),直到当前状态为 Ready
resume()
return next()
}
is State.Ready<*> -> {
// 外部调用 next() 获取数据且当前是 Ready 状态时,将状态重置为 NotReady
state = State.NotReady(currentState.continuation)
// 并把协程内部刚刚抛出并存在状态中的数据,返回给外部
(currentState as State.Ready<T>).nextValue
}
}
}
override fun hasNext(): Boolean {
// 和 next 一样恢复执行
resume()
return state != State.Done
}
// ================================
override fun resumeWith(result: Result<Any?>) {
// 在完成回调中仅变换状态
state = State.Done
result.getOrThrow()
}
其中,恢复事件由 hasNext 和 next 函数触发。如果恢复事件到达时是 NotReady 状态,就会立即恢复执行。等到生成器内部挂起或执行完成,就会进入 Ready 状态,彻底完成后调用 resumeWith 将状态转为 Done。
最后给出 GeneratorImpl 的实现进行测试。
kotlin
class GeneratorImpl<T>(
block: suspend GeneratorScope<T>.(T) -> Unit,
parameter: T
) : Generator<T> {
private val generatorIterator: GeneratorIterator<T> = GeneratorIterator(block, parameter)
override fun iterator(): Iterator<T> {
return generatorIterator
}
}
标准库中的序列生成器
Kotlin 标准库中提供了类似的生成器实现,它的使用:
kotlin
fun main() {
val sequence = sequence {
yield(1)
yield(2)
yield(3)
yield(4)
yieldAll(listOf(5, 6, 7))
}
for (element in sequence) {
println(element)
}
}
sequence() 的参数(即生成逻辑)是函数类型,该参数也是协程体。这里的 yield 作用与我们的实现相同,不同的是,它还有批量生成元素的函数 yieldAll。
我们可以使用该序列生成器来构建序列,例如斐波那契数列:
kotlin
val fibonacci = sequence {
yield(1L)
var current = 1L
var next = 1L
while (true) {
val nextTmp = next
next += current
current = nextTmp
yield(current)
}
}
fibonacci.take(10).forEach(::println)
Promise 模型
Promise 模型是最常见、最符合直觉的协程实现,我们来实现它以加深对协程的理解。
async/await 与 suspend 的设计对比
async/await 的设计中,async 函数内部可以对符合 Promise 协议的异步回调进行 await(等待),使得异步逻辑变为同步代码(回调同步化),这也是当前最流行的一种协程实现。
async 和 await 分别实现了挂起和恢复的逻辑,而 Kotlin 中的 suspend 关键字则同时包含了这两种语义。
举一个例子来说明:
kotlin
data class User(
val id: Long,
val name: String
)
// 模拟内存缓存
object MemoryCache {
private val cache = mutableMapOf<Long, User>()
fun put(userId: Long, user: User) {
cache[userId] = user
}
fun get(userId: Long): User? {
return cache[userId]
}
}
suspend fun getUser(userId: Long): User {
return getUserLocal(userId) ?: getUserRemote(userId).also {
MemoryCache.put(userId, it)
}
}
suspend fun getUserLocal(userId: Long): User? = suspendCoroutine { continuation ->
thread {
continuation.resume(MemoryCache.get(userId))
}
}
suspend fun getUserRemote(userId: Long): User = suspendCoroutine { continuation ->
thread {
Thread.sleep(1000)
continuation.resume(User(id = userId, name = "Empty $userId"))
}
}
其中 getUser 函数可以调用 getUserLocal 和 getUserRemote 这两个挂起函数,此时 suspend 充当了 Promise 中的 async;getUser 函数直接了返回异步的结果,故 suspend 又充当了 await 的角色。
仿 JavaScript 的 async/await 实现
现在,我们就来使用 Kotlin 协程来实现类似于 async/await 的复合协程。
先使用 Retrofit 定义获取用户信息的接口:
kotlin
data class User(
@SerializedName("login")
val username: String,
val id: String,
@SerializedName("name")
val nickname: String,
val url: String
)
interface GitHubApi {
@GET("users/{login}")
fun getUserCallback(@Path("login") login: String): Call<User>
}
Retrofit 的依赖引入及简单使用,可以参考我的博客:Retrofit:从入门到最佳实践。
该接口的使用效果:
kotlin
fun main() {
try {
val retrofit = Retrofit.Builder()
.baseUrl("https://api.github.com")
.addConverterFactory(GsonConverterFactory.create())
.build()
val githubApi = retrofit.create(GitHubApi::class.java)
async {
val user = await { githubApi.getUserCallback("请填入你的Github用户名") }
println(user)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
该实现相较于前面的序列生成器来说更为简单,只需要定义一个 async 函数来启动协程,在协程体中提供 await 函数来转换回调即可。
kotlin
// 唯一作用:约束 await 函数只能在 async 函数启动的协程内部调用
interface AsyncScope {
suspend fun <T> AsyncScope.await(block: () -> Call<T>) = suspendCoroutine<T> { continuation ->
val call = block()
call.enqueue(object : Callback<T> {
override fun onResponse(call: Call<T?>, resp: Response<T?>) {
if (resp.isSuccessful) {
resp.body()?.let {
continuation.resume(it)
} ?: continuation.resumeWithException(NullPointerException())
} else {
continuation.resumeWithException(HttpException(resp))
}
}
override fun onFailure(call: Call<T?>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
}
// 启动的协程不需要返回值
fun async(context: CoroutineContext = EmptyCoroutineContext, block: suspend AsyncScope.() -> Unit) {
val completion = AsyncCoroutine(context)
block.startCoroutine(receiver = object : AsyncScope {}, completion = completion)
}
class AsyncCoroutine(override val context: CoroutineContext = EmptyCoroutineContext) : Continuation<Unit> {
// 协程完成回调
override fun resumeWith(result: Result<Unit>) {
result.getOrThrow()
}
}
我们为 async 函数预留了一个协程上下文参数,之后可以为 async 启动的协程指定合适的拦截器来实现线程切换。
Lua 风格的协程 API
我们在讨论 Kotlin 协程时,总是说创建了一个简单协程,却不清楚有哪个类与之对应(就像 Java 中的 Thread 类)。在复合协程的设计案例中,我们总是将协程的状态机封装到协程的完成回调 Continuation 实例中,其中就有着协程最为关键的状态流转。因该实例提供了各种能力的封装,故也可被称为复合协程本身。
而 Lua 的 API 就很直接,只需提供一个函数,即可创建一个协程对象。接着,我们就可以利用该对象来控制协程执行。
非对称 API 实现
首先来完成 Lua 协程 API 的标准实现,使用效果如下:
kotlin
suspend fun main() {
val producer = Coroutine.create<Unit, Int>(EmptyCoroutineContext) {
for (i in 0..3) {
println("send $i")
yield(i)
}
200
}
val consumer = Coroutine.create(EmptyCoroutineContext) { param: Int ->
println("start $param")
for (i in 0..3) {
val value = yield(Unit)
println("receive $value")
}
}
while (producer.isActive && consumer.isActive) {
val result = producer.resume(Unit)
consumer.resume(result)
}
}
我们使用 Coroutine.create() 来创建协程,参数就是协程体,协程体的参数类型和返回值类型由泛型参数指定。为了弄清这里的数据流转,我们需要搞懂状态设计:
kotlin
sealed class Status {
class Created(val continuation: Continuation<Unit>) : Status()
class Yielded<P>(val continuation: Continuation<P>) : Status()
class Resumed<R>(val continuation: Continuation<R>) : Status()
object Dead : Status()
}
在这个模型中,外部调用者和协程内部像是隔着一扇窗户在传递数据:
P(Parameter) :是外部通过resume(P)灌入 给协程的数据类型。这也是协程内部yield醒来时拿到的返回值类型。R(Result) :是协程内部通过yield(R)抛出 给外部的数据类型。这也是外部调用resume等待协程挂起后拿到的返回值类型。
结合这两个泛型,我们来解释一下每个状态:
- Created: 协程创建后尚未执行的状态,需要外部调用
resume函数灌入初始参数才会开始执行。 - Yielded: 协程内部调用
yield函数向外抛出数据后的挂起状态。 - Resumed: 协程外部调用
resume函数灌入数据后的执行状态。 - Dead: 协程执行完毕的状态。
上述的状态转移流程图:
接下来创建 Coroutine 类承载并维护状态机:
kotlin
// 协程作用域
interface CoroutineScope<P, R> {
// 协程启动时传入的参数
var parameter: P?
suspend fun yield(value: R): P
}
// 协程描述类
@OptIn(ExperimentalAtomicApi::class)
class Coroutine<P, R>(
val context: CoroutineContext = EmptyCoroutineContext,
private val block: suspend CoroutineScope<P, R>.(P) -> R
) {
companion object {
// 创建协程对象的方法
fun <P, R> create(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend CoroutineScope<P, R>.(P) -> R
): Coroutine<P, R> {
return Coroutine(context, block)
}
}
val scope = object : CoroutineScope<P, R> {
override var parameter: P? = null
override suspend fun yield(value: R): P = suspendCoroutine { continuation ->
TODO("Not yet implemented")
}
}
private val completion = object : Continuation<R> {
override val context: CoroutineContext
get() = this@Coroutine.context
override fun resumeWith(result: Result<R>) {
TODO("Not yet implemented")
}
}
private val status: AtomicReference<Status>
init {
val coroutineBlock: suspend CoroutineScope<P, R>.() -> R = {
block(parameter!!)
}
val start = coroutineBlock.createCoroutine(receiver = scope, completion = completion)
status = AtomicReference(Status.Created(start))
}
val isActive: Boolean
get() = status.load() != Status.Dead
}
AtomicReference类在 Java 和 Kotlin 库都有提供,本文中使用的是 Kotlin 提供的。
注意点:
在看接下来的代码前,这里想要不迷糊,就要清楚地知道:suspendCoroutine 抓取的永远是当前正在执行它的那个协程的挂起点。 获取的 continuation 相当于当前协程的执行进度条,或是唤醒它的回调接口。
明白了这一点后,我们再来看下 yield 的实现。一定要记得:yield 的动作是协程向外抛出数据,然后自己挂起睡觉。
kotlin
// 【协程内部执行】我要挂起睡觉了,把 value(类型R) 抛到窗外去
@Suppress("UNCHECKED_CAST")
override suspend fun yield(value: R): P = suspendCoroutine { continuation ->
// 获取并更新状态
val previousStatus = status.fetchAndUpdate { oldStatus ->
when (oldStatus) {
is Status.Created -> throw IllegalStateException("Never started!")
Status.Dead -> throw IllegalStateException("Already dead!")
is Status.Resumed<*> -> Status.Yielded(continuation) // 保存内部协程的挂起点到状态机中
is Status.Yielded<*> -> throw IllegalStateException("Already yielded!")
}
}
// 恢复外部调用 resume 的那个协程,把 value 递给它
(previousStatus as? Status.Resumed<R>)?.continuation?.resume(value)
}
当该函数执行完毕后,挂起时的状态就一定是 Yielded。它调用了 previousStatus 中的 continuation,从而恢复了之前那个调用了 resume 正在等待的外部协程。
相对应,协程的 resume 函数实现,记住:resume 的动作就是外部拿着数据去敲窗户,将数据灌进去唤醒协程。
kotlin
// 【外部执行】别睡了,拿着 value(类型P) 进去继续干活
@Suppress("UNCHECKED_CAST")
suspend fun resume(value: P): R = suspendCoroutine { continuation ->
val previousStatus = status.fetchAndUpdate {
when (it) {
is Status.Created -> {
scope.parameter = value
// 保存外部协程的挂起点
Status.Resumed(continuation)
}
is Status.Yielded<*> -> Status.Resumed(continuation)
Status.Dead -> throw IllegalStateException("Already dead!")
is Status.Resumed<*> -> throw IllegalStateException("Already resumed!")
}
}
// 唤醒内部协程
when (previousStatus) {
// 如果协程刚创建,传给它初始参数,唤醒它
is Status.Created -> previousStatus.continuation.resume(Unit)
// 如果协程是挂起状态,把 value 灌进去,唤醒它
is Status.Yielded<*> -> (previousStatus as Status.Yielded<P>).continuation.resume(value)
else -> {}
}
}
最后是 resumeWith 的实现,也就是协程完成时的回调逻辑:将执行权和最终结果传给外部。
kotlin
@Suppress("UNCHECKED_CAST")
override fun resumeWith(result: Result<R>) {
val previousStatus = status.fetchAndUpdate {
when (it) {
is Status.Created -> throw IllegalStateException("Never started!")
is Status.Yielded<*> -> throw IllegalStateException("Already yielded!")
is Status.Resumed<*> -> Status.Dead
Status.Dead -> throw IllegalStateException("Already dead!")
}
}
// 把协程体 return 的最终结果,还给最后一次调用 resume 的【外部协程】
(previousStatus as? Status.Resumed<R>)?.continuation?.resumeWith(result)
}
对称 API 实现
实现对称协程,只需在非对称协程的基础上加一个"调度中心"。让每一个协程都是自由、平等的,可以随意转移调度权。
该中心需要满足:
- 在当前协程挂起时,获取调度权。
- 根据目标协程对象,来完成调度权的转移。
我们可以直接提拔一个特权协程来充当这个中心,因为它完美满足上述这两个条件。使用效果如下:
kotlin
object SymCoroutines {
val coroutine0 = SymCoroutine.create { param: Int ->
println("coroutine-0 $param")
// 使用 transfer 来完成调度权的转移
var result = transfer(coroutine2, 0)
println("coroutine-0 1 $result")
result = transfer(SymCoroutine.main, Unit)
println("coroutine-0 1 $result")
}
val coroutine1: SymCoroutine<Int> = SymCoroutine.create { param: Int ->
println("coroutine-1 $param")
val result = transfer(coroutine0, 1)
println("coroutine-1 1 $result")
}
val coroutine2: SymCoroutine<Int> = SymCoroutine.create { param: Int ->
println("coroutine-2 $param")
var result = transfer(coroutine1, 2)
println("coroutine-2 1 $result")
result = transfer(coroutine0, 2)
println("coroutine-2 2 $result")
}
}
接着在主流程中通过调度中心启动这几个协程,然后就开始了对称协程的调度权的转移过程:
kotlin
suspend fun main() {
SymCoroutine.main {
println("main 0")
// 初始时,将调度权转移给 coroutine2
val result = transfer(SymCoroutines.coroutine2, 3)
println("main end $result")
}
}
我们定义一个接口来提供 transfer 函数:
kotlin
interface SymCoroutineScope<T> {
suspend fun <P> transfer(symCoroutine: SymCoroutine<P>, value: P): T
}
其中泛型 T 是当前协程的接收返回值类型,P 则是目标对称协程的参数类型。
接着给出 SymCoroutine 的定义:
kotlin
class SymCoroutine<T>(
override val context: CoroutineContext = EmptyCoroutineContext,
private val block: suspend SymCoroutineScope<T>.(T) -> Unit
) : Continuation<T> {
companion object {
lateinit var main: SymCoroutine<Any?>
suspend fun main(
block: suspend SymCoroutineScope<Any?>.() -> Unit
) {
SymCoroutine<Any?> {
// 执行 main { ... } 里写的逻辑
block()
}.also {
main = it
}.start(Unit) // 开始唤醒
}
// 只是为了创建 SymCoroutine 对象
fun <T> create(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend SymCoroutineScope<T>.(T) -> Unit
): SymCoroutine<T> {
return SymCoroutine(context, block)
}
}
val isMain: Boolean
get() = this == main
override fun resumeWith(result: Result<T>) {
TODO("Not yet implemented")
}
}
我们在 SymCoroutine.main 中创建了特殊协程作为调度中心,并保存下来,以便其他协程归还调度权时使用。
SymCoroutine 内部非对称协程的定义:要想让出调度权,当前协程只需要调用内部非对称协程的 yield 挂起自己,并将"下一个要唤醒的协程和数据"打包成 Parameter 抛给特殊协程即可。
kotlin
class Parameter<T>(
val coroutine: SymCoroutine<T>,
val value: T
)
// =========================
@Suppress("UNCHECKED_CAST")
// 非对称协程
private val coroutine = Coroutine<T, Parameter<*>>(context) {
// 这里的代码,就是这个非对称协程的【协程体】
Parameter(
this@SymCoroutine,
suspend {
// 执行我们写在 create { ... } 里的业务逻辑
block(scope, it)
if (this@SymCoroutine.isMain) Unit
else {
throw IllegalStateException("SymCoroutine cannot be dead.")
}
}() as T
)
}
override fun resumeWith(result: Result<T>) {
throw IllegalStateException("SymCoroutine cannot be dead!")
}
// 专门用来唤醒内部的非对称协程
suspend fun start(value: T) {
coroutine.resume(value)
}
最后是最关键的 transfer 函数的实现。这一步是整个对称调度的精髓。
kotlin
private val scope: SymCoroutineScope<T> =
object : SymCoroutineScope<T> {
@Suppress("UNCHECKED_CAST")
private tailrec suspend fun <P> transferInner(
symCoroutine: SymCoroutine<P>,
value: Any?
): T {
if (this@SymCoroutine.isMain) {
return if (symCoroutine.isMain) {
value as T
} else {
// ... 1. 特权协程拿着数据去唤醒目标协程
val parameter =
symCoroutine.coroutine.resume(value as P)
// 递归处理下一个调度请求
transferInner(parameter.coroutine, parameter.value)
}
} else {
with(coroutine.scope) {
// ... 2. 普通协程交出调度权,把请求打包抛给特权协程
return yield(
Parameter(symCoroutine, value as P)
)
}
}
}
override suspend fun <P> transfer(
symCoroutine: SymCoroutine<P>,
value: P
): T {
return transferInner(symCoroutine, value)
}
}
我们详细拆解一下 transfer 是如何巧妙地转移调度权的:
-
首先程序开始执行时,调度权在控制中心(也就是
SymCoroutine.main这个特权协程)手中。它执行了transfer(coroutine2, 3),因为当前协程是Main,并且目标协程不是Main,所以Main协程会主动唤醒coroutine2,并且将数据3灌进去。然后Main会挂起等待coroutine2的结果。 -
coroutine2醒来后,会执行打印,并调用transfer(coroutine1, 2)。此时,因为当前协程不是Main协程,所以会将自己挂起,并将目标协程(coroutine1)和附带的数据(2)打包成Parameter对象,返回给调用者:Main协程。 -
Main协程收到数据后,就会将返回值赋值给parameter。然后进行尾递归调用,Main会再次进入步骤 1 的逻辑,作为中心去唤醒coroutine1。 -
以此类推,调度权会进行不断地转移,但每次转移都会经过
Main协程。最终coroutine0执行transfer(SymCoroutine.main, Unit),在下一次递归中,因为目标协程是自己,所以会直接返回(return value as T)。
至此,程序结束。