深入理解 Kotlin 协程 (四):大道至简,于微末的挂起恢复中衍化万物

前言

本文将使用 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)
}

上述代码的执行流程如时序图所示:

sequenceDiagram autonumber participant Main as 主协程(Main) participant Generator as 生成器协程(Generator) %% 初始状态 Note over Main: 初始状态:Idle(空闲) Note over Generator: 初始状态:Created(已创建) %% 第一步:主协程运行 -> loop@0 挂起 Main->>Main: 转为 Running(运行) Note over Main: 执行 loop@0
主动挂起 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 中包含着迭代器,调用迭代器的 hasNextnext 都会触发下一个元素的获取,挂起的逻辑自然属于该迭代器的内部状态:

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: 生成器执行完毕,后续将无新元素产生。

这三种状态的流转图:

stateDiagram-v2 [*] --> NotReady NotReady --> Ready: yield(i) Ready --> NotReady: resume() NotReady --> Done: resume() 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()
}

其中,恢复事件由 hasNextnext 函数触发。如果恢复事件到达时是 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(等待),使得异步逻辑变为同步代码(回调同步化),这也是当前最流行的一种协程实现。

asyncawait 分别实现了挂起和恢复的逻辑,而 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 函数可以调用 getUserLocalgetUserRemote 这两个挂起函数,此时 suspend 充当了 Promise 中的 asyncgetUser 函数直接了返回异步的结果,故 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: 协程执行完毕的状态。

上述的状态转移流程图:

stateDiagram-v2 [*] --> Created Created --> Resumed Resumed --> Yielded: yield(r) Yielded --> Resumed: resume(p) Resumed --> Dead: end of body 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 实现

实现对称协程,只需在非对称协程的基础上加一个"调度中心"。让每一个协程都是自由、平等的,可以随意转移调度权。

该中心需要满足:

  1. 在当前协程挂起时,获取调度权。
  2. 根据目标协程对象,来完成调度权的转移。

我们可以直接提拔一个特权协程来充当这个中心,因为它完美满足上述这两个条件。使用效果如下:

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 是如何巧妙地转移调度权的:

  1. 首先程序开始执行时,调度权在控制中心(也就是 SymCoroutine.main 这个特权协程)手中。它执行了 transfer(coroutine2, 3),因为当前协程是 Main,并且目标协程不是 Main,所以 Main 协程会主动唤醒 coroutine2,并且将数据 3 灌进去。然后 Main 会挂起等待 coroutine2 的结果。

  2. coroutine2 醒来后,会执行打印,并调用 transfer(coroutine1, 2)。此时,因为当前协程不是 Main 协程,所以会将自己挂起,并将目标协程(coroutine1)和附带的数据(2)打包成 Parameter 对象,返回给调用者:Main 协程。

  3. Main 协程收到数据后,就会将返回值赋值给 parameter。然后进行尾递归调用,Main 会再次进入步骤 1 的逻辑,作为中心去唤醒 coroutine1

  4. 以此类推,调度权会进行不断地转移,但每次转移都会经过 Main 协程。最终 coroutine0 执行 transfer(SymCoroutine.main, Unit),在下一次递归中,因为目标协程是自己,所以会直接返回(return value as T)。

至此,程序结束。

相关推荐
jinanwuhuaguo12 小时前
OpenClaw 2026年4月升级大系深度解读剖析:从“架构重塑”到“信任内建”的范式跃迁
android·开发语言·人工智能·架构·kotlin·openclaw
我命由我1234514 小时前
Android Jetpack Compose - SearchBar(搜索栏)、Tab(标签页)、时间选择器、TooltipBox(工具提示)
android·java·java-ee·kotlin·android studio·android jetpack·android-studio
Lsk_Smion14 小时前
Hot100(开刷) 之 长度最小的数组--删除倒数第N个链表--层序遍历
java·数据结构·算法·kotlin
我命由我1234516 小时前
Android Jetpack Compose - 组件分类:布局组件、交互组件、文本组件
android·java·java-ee·kotlin·android studio·android jetpack·android-studio
Kapaseker17 小时前
让你的 App 成为 AI 的一环
android·kotlin
千码君20161 天前
kotlin:Jetpack Compose 给APP添加声音(点击音效/背景音乐)
android·开发语言·kotlin·音效·jetpack compose
Kapaseker2 天前
lazy 与 lateinit 到底有什么区别?
android·kotlin
雨白2 天前
深入理解 Kotlin 协程 (三):返璞归真,探寻协程基础设施的底层基石
kotlin
call me by ur name2 天前
ERNIE 5.0 Technical Report论文解读
android·开发语言·人工智能·机器学习·ai·kotlin