深入理解 Kotlin 协程 (五):偷天换日,探秘状态机与调度的运转引擎

前言

本文及下一篇文章,将基于 Kotlin 协程的基础设施封装一套简易的协程业务框架(CoroutineLite)。

官方协程的设计思路:github.com/Kotlin/KEEP...

实现非阻塞的 delay 函数

如果我们想要在线程中延迟一段时间,通常会使用 Thread.sleep 函数,但这会阻塞当前线程。

在协程中同样可以使用 Thread.sleep,不过这仍然会阻塞当前线程,因为归根到底协程的代码最终会运行在操作系统的内核线程上。正确的做法是:将协程挂起,等到指定的时间之后再恢复执行,这样就不会阻塞当前线程,从而让该线程可以去执行其他任务。

所以 delay 要实现以下两点:

  1. 能够不阻塞线程。
  2. 是一个挂起函数,在指定的时间之后能够恢复执行。

先给出函数的声明:

kotlin 复制代码
// [Delay.kt]
suspend fun delay(time: Long, unit: TimeUnit = TimeUnit.MILLISECONDS) {
    if (time <= 0) {
        // 表示无需延迟,直接返回
        return
    }
    // ...
}

考虑挂起的话,我们很容易想到标准库中的 suspendCoroutine 函数就行,而恢复执行就是调用 continuation.resume

关键在于怎么提供定时回调的机制?当然你可以启动一个线程,在睡眠指定时间后进行调用。但在 JVM 平台上,我们可以使用 ScheduledExecutorService,像这样:

kotlin 复制代码
// [Delay.kt]
private val executor = Executors.newScheduledThreadPool(1) { r ->
    Thread(r, "Scheduler").apply {
        // 设置为幽灵线程(守护线程)
        isDaemon = true
    }
}

suspend fun delay(time: Long, unit: TimeUnit = TimeUnit.MILLISECONDS) {
    if (time <= 0) {
        // 表示无需延迟,直接返回
        return
    }

    suspendCoroutine { continuation ->
        // 在指定时间后,由后台线程执行恢复操作
        executor.schedule({ continuation.resume(Unit) }, time, unit)
    }
}

我们将新创建的线程设置为了幽灵线程(Daemon Thread),这样调度器在后台空载时,可以不阻碍虚拟机的正常退出(当 JVM 中只剩下幽灵线程时,虚拟机会自动退出)。

至此,我们就实现了协程框架中最常用的 delay 函数。不过,使用 ScheduledExecutorService 就不会阻塞线程了吗,它是怎么做到的?

它当然也会在等待延时事件时阻塞后台线程,不过该后台线程可以同时承载非常多的延时任务。例如有 10 个协程调用了 delay,也只需阻塞这一个后台线程来完成这 10 个协程的延迟执行。这种方式比我们每次调用 delay 都去启动一个新线程要高效得多,极大地提升了线程资源的利用率。

注意:

  1. 后台线程恢复协程执行时,会有一定的调度延迟。
  2. 在不同平台上,delay 的延迟实现也不同,例如在 JavaScript 中可以利用 setTimeout 来实现。

协程的描述与状态抽象

在业务开发中,直接使用底层的 startCoroutinecreateCoroutine 稍显复杂。所以框架中专门针对不同场景提供了不同的协程构造器(Builder,如 launch、async 等)。

定义 Job 接口

协程具体是什么,官方的设计者并没有把协程明确的类型放入到标准库中。为了降低管理成本,我们需要引入一个类型来描述协程本身,并向外部提供 API 来控制它的执行。

这个类的命名 Job 参考自官方框架,以下是它的定义:

kotlin 复制代码
// [Job.kt]
interface Job : CoroutineContext.Element {
    companion object Key : CoroutineContext.Key<Job>

    override val key: CoroutineContext.Key<*>
        get() = Job

    /**
     * 用于查询协程是否仍在执行
     */
    val isActive: Boolean

    /**
     * 注册协程被取消时触发的回调
     */
    fun invokeOnCancel(onCancel: OnCancel): Disposable

    /**
     * 注册协程完成时的回调
     */
    fun invokeOnCompletion(onComplete: OnComplete): Disposable

    /**
     * 用于取消协程
     */
    fun cancel()

    /**
     * 移除上述已注册的回调
     */
    fun remove(disposable: Disposable)

    /**
     * 挂起当前协程,直到目标协程完成
     */
    suspend fun join()
}

/**
 * 表示可释放的资源控制对象
 */
interface Disposable {
    /**
     * 释放资源、移除回调
     */
    fun dispose()
}

typealias OnComplete = () -> Unit
typealias OnCancel = () -> Unit

通过让 Job 实现 CoroutineContext.Element 接口,并利用其伴生对象作为"键",我们就可以将 Job 实例存入协程上下文中。这样一来,只要我们能够获取协程上下文,就可以轻松地拿到代表该协程的 Job 实例。

协程的状态机模型

协程启动之后的状态主要有这么几种:未完成、取消中、已完成。

状态的定义如下:

kotlin 复制代码
// [CoroutineState.kt]
sealed class CoroutineState {
    /**
     * 协程启动后立即进入该状态,直到完成或者被取消
     */
    class Incomplete : CoroutineState()

    /**
     * 协程执行中被取消后进入该状态
     */
    class Cancelling : CoroutineState()

    /**
     * 协程执行完成(包括正常返回和异常结束)时进入该状态
     * @param value 正常执行成功的返回结果
     * @param exception 执行失败或抛出取消异常时的错误对象
     */
    class Complete<T>(val value: T? = null, val exception: Throwable? = null) : CoroutineState()
}

我们进一步解释一下 Cancelling 状态:

进入该状态后,要等待协程体内部的挂起函数调用 响应取消。响应后协程成功被取消并抛出 CancellationException,否则会正常执行完成。这两种情况最终都会调用完成回调,将状态流转为 Complete

状态流转与并发安全的回调

注册协程的回调时,需要根据当前的状态采取不同的处理方式,例如当前是 Complete 状态,那么完成回调就需要立即触发。回调会随着状态进行流转,例如上一个状态注册的回调,在状态变为"取消中"时,需要被安全地传递或触发。

什么是并发安全(线程安全)?

在多线程或多协程环境下,如果有多个执行流同时操作同一个共享变量,就容易发生数据被相互覆盖、状态错乱的情况。要保证并发安全,需要同时满足以下三点:

  1. 原子性:确保"读-改-写"这种复合操作是一个不可分割的整体(即原子性),在执行过程中不会被打断,从而避免更新冲突。

  2. 可见性:CPU 有多级缓存。要确保一个线程对共享变量的修改,能够立即同步到主内存中,让其他线程看到,而不只存在于当前线程的本地缓存中。

  3. 有序性:编译器和 CPU 为了性能常常会对指令进行重排。我们要防止这种重排序破坏多线程间的逻辑依赖,避免读取到还未初始化的错误状态。

注册的操作也必须是原子操作,否则可能会导致状态不一致。

例如注册取消回调时,Caller-A 获取的状态是 Incomplete,接着开始注册,但在注册完成前,Caller-B 将协程取消了,这就会导致 Caller-A 在向一个处于"取消中"状态的协程注册"未完成"回调,从而出现状态不一致的问题。

sequenceDiagram participant A as Caller-A participant B as Caller-B participant C as Coroutine Note over C: Incomplete A->>C: getState activate C C->>A: Incomplete deactivate C B-->>C: cancel Note over C: Cancelling A-->>C: invokeOnCancel

为了保证原子性,我们可以选择加锁,但这会产生较大的开销。为了提升性能,我们使用原子类来处理并发操作,例如:

kotlin 复制代码
// [AbstractCoroutine.kt]
// 注意:本文使用的是 Kotlin 相关的 Atomic API (如 updateAndFetch)。
// 若在纯 Java 中,可以使用 java.util.concurrent.atomic.AtomicReference 的 updateAndGet 方法。
@OptIn(ExperimentalAtomicApi::class)
protected val state = AtomicReference<CoroutineState>(value = CoroutineState.Incomplete())

@OptIn(ExperimentalAtomicApi::class)
override fun cancel() {
    // 获取当前状态,并将状态安全地更新为 Lambda 块返回的新状态
    val newState = state.updateAndFetch { prev ->
        when (prev) {
            // 根据旧状态 prev 返回对应的新状态
        }
    }
    // ...
}

原子类是如何实现以上三大特性的?

实现原子性: 首先它会依赖 CPU 直接提供的原子指令(核心是 CAS,Compare-And-Swap),这是硬件级别的单步指令。在执行时,CPU 会锁住总线或缓存行,确保"读取旧值、比对旧值、写入新值"的过程是一气呵成的。

updateAndFetch 函数为例,它会保证当前的修改是原子性的。其内部逻辑为:如果获取当前状态后、更新新状态前,该状态被其他执行流修改了,它就会重试并重复执行 Lambda 块。在比对、写入时,就用到了 compareAndSet 这个函数。

实现可见性和有序性: 为了保证原子指令正确运行,编译器和 CPU 硬件会在执行原子操作的前后,插入特殊的底层指令------内存屏障。

它会强制让当前线程修改后的值立即同步到主内存,让其他线程的缓存中的该变量的副本失效。并且告知编译器和 CPU 硬件,屏障前后的指令不能交叉重排,这就避免了引发错乱的指令重排。

由于在状态进行流转时需要传递回调,为了避免并发修改带来的问题,存储注册回调的数据结构也必须是并发安全的,我们使用了一个具有不可变性的递归列表。

这种不可变模式也是实现并发安全的一种途径,它初始化后就变为只读,根本没有写操作,天生绝对安全。

kotlin 复制代码
// [DisposableList.kt]
/**
 * 递归列表
 */
sealed class DisposableList {
    // 链表的末端(空元素)
    object Nil : DisposableList()

    // 链表的节点,包含当前元素 head 和指向下一个节点的 tail
    class Cons(
        val head: Disposable,
        val tail: DisposableList
    ) : DisposableList()
}

/**
 * 递归访问列表
 * tailrec: 启用尾递归优化,将递归函数在编译时转为循环结构,避免栈溢出
 */
tailrec fun DisposableList.forEach(action: (Disposable) -> Unit): Unit =
    when (this) {
        // 结束
        DisposableList.Nil -> Unit
        is DisposableList.Cons -> {
            // 1. 先访问该列表的头元素
            action(this.head)
            // 2. 递归访问后续的列表
            this.tail.forEach(action)
        }
    }

/**
 * 移除某个元素,返回一个新的不包含该元素的递归列表
 */
fun DisposableList.remove(disposable: Disposable): DisposableList {
    return when (this) {
        DisposableList.Nil -> this
        is DisposableList.Cons -> {
            if (head == disposable) {
                return tail
            } else {
                // 递归移除,重新构建节点
                DisposableList.Cons(head, tail.remove(disposable))
            }
        }
    }
}

/**
 * 筛选并遍历特定类型的回调
 */
inline fun <reified T : Disposable> DisposableList.loopOn(
    crossinline action: (T) -> Unit
) = forEach {
    when (it) {
        is T -> action(it)
    }
}

以存储三个回调元素为例,下图展示了其完整的数据形态:

接着,我们将这个数据结构添加状态中,在状态发生变化时,上一个状态的回调可以传递给新状态

但你会发现内部的这个 disposableList 变量是可变的 var,如果我们允许外部直接在原状态对象上修改它,就可能会导致回调被覆盖:例如两个线程同时读到了旧链表,各自创建新节点后先后赋值,后执行的就会消除前者的修改。为了杜绝这个隐患,我们规定:如果我们要添加或移除回调,绝对不允许直接修改原对象,而是必须隔离创建一个全新的状态对象,一旦新对象通过 CAS 替换成功并发布,就不会再有并发安全问题。

kotlin 复制代码
// [CoroutineState.kt]
sealed class CoroutineState {
    private var disposableList: DisposableList = DisposableList.Nil

    /**
     * 从上一个状态中继承所有的回调列表
     */
    fun from(state: CoroutineState): CoroutineState {
        this.disposableList = state.disposableList
        return this
    }

    /**
     * 注册新的回调元素,构造一个新的链表头节点
     */
    fun with(disposable: Disposable): CoroutineState {
        this.disposableList = DisposableList.Cons(
            head = disposable, // 新元素
            tail = this.disposableList
        )
        return this
    }

    /**
     * 移除特定回调元素
     */
    fun without(disposable: Disposable): CoroutineState {
        this.disposableList = this.disposableList.remove(disposable)
        return this
    }

    /**
     * 清空所有回调
     */
    fun clear() {
        this.disposableList = DisposableList.Nil
    }
    
    // ...
}

协程基类的实现 (AbstractCoroutine)

状态机定义好后,剩下的就是为状态机输入事件了,我们为 Job 定义一个抽象实现类:

kotlin 复制代码
// [AbstractCoroutine.kt]
@OptIn(ExperimentalAtomicApi::class)
abstract class AbstractCoroutine<T>(context: CoroutineContext) : Job, Continuation<T> {

    // 初始化时状态为 Incomplete
    protected val state = AtomicReference<CoroutineState>(CoroutineState.Incomplete())

    // 将自身(Job 实例)添加到协程上下文中
    override val context: CoroutineContext = context + this

    val isCompleted
        get() = state.load() is CoroutineState.Complete<*>

    override val isActive: Boolean
        get() = when (state.load()) {
            is CoroutineState.Complete<*>,
            is CoroutineState.Cancelling -> false

            else -> true
        }
}

协程的构建与等待机制

实现 launch (无返回值)

如果一个协程的返回值是 Unit,对于这种"即发即忘"的协程,我们只需启动即可。

kotlin 复制代码
launch {
    (60 downTo 1).forEach {
        println(it)
        delay(1000)
    }
}

launch 函数的实现为:

kotlin 复制代码
// [Builders.kt]
fun launch(
    context: CoroutineContext = EmptyCoroutineContext,
    block: suspend () -> Unit
): Job {
    // completion 既作为协程完成后的回调,又是协程的描述类
    val completion = StandaloneCoroutine(context)
    block.startCoroutine(completion)
    return completion
}

StandaloneCoroutineAbstractCoroutine 的子类,其实现为:

kotlin 复制代码
// [StandaloneCoroutine.kt]
class StandaloneCoroutine(context: CoroutineContext = EmptyCoroutineContext) : AbstractCoroutine<Unit>(context)

注册与触发完成回调

如果想要知道由 launch 创建的协程什么时候结束,可以通过注册 OnComplete 回调来完成。这需要做到两件事:

  1. 将回调注册到协程中,同时支持回调的移除。
  2. 在协程完成时通知这些回调。

我们先来看看怎么注册:

kotlin 复制代码
// [AbstractCoroutine.kt]
override fun invokeOnCompletion(onComplete: OnComplete): Disposable {
    // 注册单纯通知结束的回调(不需要具体结果)
    return doOnCompleted { _ -> onComplete() }
}

protected fun doOnCompleted(block: (Result<T>) -> Unit): Disposable {
    // 1. 构造一个控制回调移除的 Disposable 对象
    val disposable = CompletionHandlerDisposable(this, block)

    // 2. 检查状态,并更新回调列表
    val newState = state.updateAndFetch { prev ->
        when (prev) {
            is CoroutineState.Incomplete -> {
                // 新增回调
                CoroutineState.Incomplete().from(prev).with(disposable)
            }

            is CoroutineState.Cancelling -> {
                // 新增回调
                CoroutineState.Cancelling().from(prev).with(disposable)
            }
            // 如果已经是完成状态,则保持不变
            is CoroutineState.Complete<*> -> prev
        }
    }

    // 3. 流转成功后,如果当前已经是 Complete 状态,这表明新回调没有也没必要被注册进去
    // 需要立即触发该回调
    (newState as? CoroutineState.Complete<T>)?.let {
        val result = when {
            it.value != null -> Result.success(it.value)
            it.exception != null -> Result.failure(it.exception)
            else -> throw IllegalStateException("Won't happen.")
        }
        // 立即回调
        block(result)
    }

    return disposable
}

移除回调的实现很简单,只需要调用 CompletionHandlerDisposable 对象中的 dispose 函数即可:

kotlin 复制代码
// [CompletionHandlerDisposable.kt]
class CompletionHandlerDisposable<T>(
    val job: Job,
    val onComplete: (Result<T>) -> Unit
) : Disposable {
    override fun dispose() {
        // 调用 Job 中的 remove 来移除自身
        job.remove(disposable = this@CompletionHandlerDisposable)
    }
}

其中 Job 中的移除实现为:

kotlin 复制代码
// [AbstractCoroutine.kt]
override fun remove(disposable: Disposable) {
    state.update { prev ->
        return@update when (prev) {
            is CoroutineState.Incomplete -> {
                // 创建一个不包含该回调的新 Incomplete 状态
                CoroutineState.Incomplete().from(prev).without(disposable)
            }

            is CoroutineState.Cancelling -> {
                CoroutineState.Cancelling().from(prev).without(disposable)
            }

            is CoroutineState.Complete<*> -> {
                // 已经完成,无需处理
                prev
            }
        }
    }
}

接着,我们来完成事件的通知。我们在启动协程时,将 AbstractCoroutine 实例( StandaloneCoroutine)作为 completion 参数进行传入,所以当协程执行完成后,会执行其 resumeWith 函数,我们只需将状态流转为完成状态,并通知之前注册的回调即可。

kotlin 复制代码
// [AbstractCoroutine.kt]
override fun resumeWith(result: Result<T>) {
    val newState = state.updateAndFetch { prevState ->
        when (prevState) {
            is CoroutineState.Cancelling,
            is CoroutineState.Incomplete -> {
                // 流转为完成状态,并携带执行结果或异常
                CoroutineState.Complete(
                    value = result.getOrNull(), // 对于无返回值的协程,这里是 Unit
                    exception = result.exceptionOrNull()
                ).from(prevState)
            }

            is CoroutineState.Complete<*> -> {
                throw IllegalStateException("Already completed!")
            }
        }
    }
    // 此时新状态一定为 Complete,调用通知回调
    newState.notifyCompletion(result)
    // 清空回调列表
    newState.clear()
}

通知逻辑的代码:

kotlin 复制代码
// [CoroutineState.kt]
fun <T> notifyCompletion(result: Result<T>) {
    // 遍历列表中的完成回调并触发
    this.disposableList.loopOn<CompletionHandlerDisposable<T>> {
        it.onComplete(result)
    }
}

实现 join (挂起等待)

join 是一个挂起函数,调用它会挂起等待协程的执行。实际上它就是一个回调转协程的例子,我们只要使用 suspendCoroutine 挂起,然后当目标协程注册一个完成回调,当目标协程完成后,恢复当前协程的执行就行。

代码实现如下:

kotlin 复制代码
// [AbstractCoroutine.kt]
override suspend fun join() {
    when (state.load()) {
        is CoroutineState.Incomplete,
        is CoroutineState.Cancelling -> {
            // 被等待的协程尚未完成,挂起等待
            return joinSuspend()
        }

        is CoroutineState.Complete<*> -> {
            // 被等待的协程已经完成,立即返回,不挂起
            return
        }
    }
}

private suspend fun joinSuspend() = suspendCoroutine { continuation ->
    // 向目标协程注册一个完成回调
    doOnCompleted { _ ->
        // 当目标协程执行完毕后,恢复 join 调用处的后续执行流
        continuation.resume(Unit)
    }
}

注意:

如果 join 所在的协程取消,那么 join 会立即抛出 CancellationException 来响应取消,这点会在后续文章中继续完善。

实现 async 与 await (有返回值)

如果还想拿到协程的返回值,我们可以在 Job 的基础上再定义一个接口 Deferred

kotlin 复制代码
// [Deferred.kt]
interface Deferred<T> : Job {
    // T 为返回值类型
    suspend fun await(): T
}

调用 await 可以在协程执行完成时,立即拿到协程的结果(如果出现异常,则会进行抛出)。如果协程未完成,则会挂起直到执行完成。

另外,我们定义 DeferredCoroutine 类来实现这个接口,同时继承自 AbstractCoroutine 这个抽象类,因为它已经实现了协程通用的逻辑。

kotlin 复制代码
// [DeferredCoroutine.kt]
@OptIn(ExperimentalAtomicApi::class)
class DeferredCoroutine<T>(context: CoroutineContext) : AbstractCoroutine<T>(context), Deferred<T> {

    override suspend fun await(): T {
        return when (val currentState = state.load()) {
            is CoroutineState.Incomplete,
            is CoroutineState.Cancelling -> awaitSuspend()

            is CoroutineState.Complete<*> -> {
                // 如果已完成且有异常,直接抛出;否则返回正常结果
                currentState.exception?.let { throw it }
                    ?: (currentState.value as T)
            }
        }
    }

    private suspend fun awaitSuspend() = suspendCoroutine { continuation ->
        doOnCompleted { result ->
            // 将目标协程执行的结果恢复给等待者
            continuation.resumeWith(result)
        }
    }

    // 省略 invokeOnCancel、cancel 等实现
}

可以看出,await 其实就是在 join 挂起恢复的基础思路上,增加了对协程执行结果的处理逻辑。

顺势给出 async 函数的实现:

kotlin 复制代码
// [Builders.kt]
fun <T> async(
    context: CoroutineContext = EmptyCoroutineContext,
    block: suspend () -> T
): Deferred<T> {
    val completion = DeferredCoroutine<T>(context)
    block.startCoroutine(completion)
    return completion
}

使用示例:

kotlin 复制代码
suspend fun main() {
    val deferred = async {
        delay(3000L)
        return@async "Android"
    }
    // 等待协程执行完毕,并获取返回值
    val result = deferred.await()
    println("result is $result")
}

协程的调度机制

拦截器与调度原理

不同于线程由操作系统进行抢占式调度,协程被称为"用户态线程"。协程的调度是由开发者自己决定的,我们在需要挂起的位置进行调度。

挂起并引发异步的情况,通常有这几种:

  1. 挂起点对应的挂起函数在内部切换了线程,并在该线程中调用了 Continuation.resume 来恢复执行。
  2. 挂起函数内部通过事件循环机制将恢复调用转到新的线程调用栈上去执行,这个过程不一定发生了真实的线程切换。
  3. 挂起函数内部保存了 Continuation 实例,后续在某个合适的时机再执行恢复调用。(这个过程也不一定发生线程切换,但函数调用栈会发生变化)

总而言之,只要挂起点的恢复调用与挂起操作不在同一个函数调用栈上执行,就会发生真正的挂起。而只有当挂起点真正挂起后,我们才有机会通过协程上下文的"拦截器"来实现调度。

拦截器的本质就是代理现有的 Continuation 实例,以修改挂起点恢复之后的运行环境。据此,我们给出调度器的接口定义:

kotlin 复制代码
// [Dispatcher.kt]
interface Dispatcher {
    // 解决协程逻辑在哪里运行的问题
    fun dispatch(block: () -> Unit)
}

将拦截器和调度器一起使用:

kotlin 复制代码
// [DispatcherContext.kt]
open class DispatcherContext(private val dispatcher: Dispatcher) :
    AbstractCoroutineContextElement(ContinuationInterceptor),
    ContinuationInterceptor {

    // 拦截原有的 continuation
    override fun <T> interceptContinuation(continuation: Continuation<T>)
            : Continuation<T> = DispatchedContinuation(continuation, dispatcher)
}

private class DispatchedContinuation<T>(
    val delegate: Continuation<T>,
    val dispatcher: Dispatcher
) : Continuation<T> {
    override val context = delegate.context

    // 代理恢复调用逻辑,将其派发到指定的调度器上执行
    override fun resumeWith(result: Result<T>) {
        dispatcher.dispatch {
            delegate.resumeWith(result)
        }
    }
}

为什么拦截器能够切换执行环境?

核心逻辑在于 resume 的调用等于继续执行协程体,哪个线程最后真正调用了原生状态机的 resume,协程体后续的代码就在哪个线程中运行 。所以当 resume 在调度器指定的环境中被触发时,协程体也就切换到了预期的执行环境。

实现基于线程池的调度器

在 Java 平台上,最常见的调度方式就是切到某些线程上执行,官方协程框架的默认调度器就是这么做的。

基于线程池的简单调度器的实现:

kotlin 复制代码
// [DefaultDispatcher.kt]
object DefaultDispatcher : Dispatcher {
    private val threadGroup = ThreadGroup("DefaultDispatcher")
    private val threadIndex = AtomicInteger(0)

    // 创建一个固定大小的线程池,用于服务 CPU 密集型任务
    private val executor = Executors.newFixedThreadPool(
        Runtime.getRuntime().availableProcessors() + 1
    ) { runnable ->
        // 线程的创建逻辑
        Thread(
            threadGroup, runnable,
            "${threadGroup.name}-worker-${threadIndex.getAndIncrement()}"
        ).apply {
            // 设置为守护线程
            isDaemon = true
        }
    }

    override fun dispatch(block: () -> Unit) {
        // 提交到线程池中执行
        executor.submit(block)
    }
}

为了方便使用,我们用 Dispatchers 单例对象来持有默认调度器的实例:

kotlin 复制代码
// [Dispatchers.kt]
object Dispatchers {
    val Default by lazy {
        DispatcherContext(DefaultDispatcher)
    }
}

有了调度器之后,我们来为协程添加调度的能力:

kotlin 复制代码
suspend fun main() {
    println("${Thread.currentThread().name}")
    val deferred = async(Dispatchers.Default) {
        println("${Thread.currentThread().name}-Start")
        delay(1000)
        println("${Thread.currentThread().name}-End")
    }
    deferred.await()
}

如果我们使用的是默认的 EmptyCoroutineContext(空上下文),那么将不会发生调度器拦截。

也就是说 println("Start") 会直接运行在启动协程所在的线程上(即 launch 被调用的线程),而 println("End") 则会运行在 delay 函数内部挂起 1000ms 后,底层定时器回调恢复执行时所在的后台线程上。

这也是为什么我们要引入调度器的原因,它能确保协程恢复执行时,按照我们的期望回到指定的线程/线程池上运行。

实现基于 UI 线程的调度器

在 Android 等平台上,要将协程调度到主线程上,只需要使用主线程的 Handler 发送一个 post 消息来执行恢复后的代码块即可。

kotlin 复制代码
// [AndroidDispatcher.kt]
object AndroidDispatcher : Dispatcher {
    private val handler = Handler(Looper.getMainLooper())
    override fun dispatch(block: () -> Unit) {
        handler.post(block)
    }
}

注入默认调度器

我们可以在开发者没有主动配置调度器时,统一为其注入默认的调度器:

kotlin 复制代码
// [Builders.kt]
fun newCoroutineContext(context: CoroutineContext): CoroutineContext {
    // 注入全局唯一的 CoroutineName 便于调试
    val combined = context +
            CoroutineName("@coroutine#${UUID.randomUUID()}")

    // 如果上下文中不存在拦截器,且未主动指定 Default,则强制追加默认调度器
    return if (combined !== Dispatchers.Default
        && combined[ContinuationInterceptor] == null
    ) {
        combined + Dispatchers.Default
    } else combined
}

// [CoroutineName.kt]
class CoroutineName(val name: String) : CoroutineContext.Element {
    companion object Key : CoroutineContext.Key<CoroutineName>

    override val key = Key

    override fun toString(): String {
        return name
    }
}

launchasync 中使用:

kotlin 复制代码
fun launch(
    context: CoroutineContext = EmptyCoroutineContext,
    block: suspend () -> Unit
): Job {
    val completion = StandaloneCoroutine(newCoroutineContext(context))
    block.startCoroutine(completion)
    return completion
}

fun <T> async(
    context: CoroutineContext = EmptyCoroutineContext,
    block: suspend () -> T
): Deferred<T> {
    val completion = DeferredCoroutine<T>(newCoroutineContext(context))
    block.startCoroutine(completion)
    return completion
}
相关推荐
CeshirenTester1 天前
Claude Code 不只是会写代码:这 10 个 Skills,才是效率分水岭
android·开发语言·kotlin
朝星1 天前
Android开发[2]:Flow
android·kotlin
alexhilton2 天前
Compose中初始加载逻辑究竟应该放在哪里?
android·kotlin·android jetpack
zhangphil2 天前
Android Coil3图片解码Bitmap后存入磁盘,再次加载读磁盘Bitmap缓存
android·kotlin
书中有颜如玉2 天前
Kotlin Coroutines 异步编程实战:从原理到生产级应用
android·开发语言·kotlin
Kapaseker3 天前
Kotlin 的 internal 修饰符到底咋回事儿?
android·kotlin
Trustport3 天前
ArcGIS Maps SDK For Kotlin 加载Layout中的MapView出错
android·开发语言·arcgis·kotlin
好家伙VCC3 天前
# ARCore+ Kotlin 实战:打造沉浸式增强现实交互应用在
java·python·kotlin·ar·交互
catoop4 天前
Kotlin 协程在 Android 开发中的应用:定义、优势与对比
android·kotlin