Kotlin-挂起函数挂起了啥?

841 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第 2 天,点击查看活动详情


不瞒各位,笔者在刚开始学习 Kotlin 协程的时候看到挂起函数总是想到阻塞线程,为啥呀?因为从表面上看,执行挂起函数的时候,后面的代码就不执行了,并且大部分挂起函数都是耗时的,这与在线程中执行耗时操作会阻塞线程类似,举个栗子:

GlobalScope.launch {
    
    /**
     * 挂起函数前的代码
     */
    println("1:${Thread.currentThread().name}") // ① 标记1
    
    /**
     * 执行挂起函数
     */
    delay(1000)
    
    /**
     * 挂起函数后的代码
     */
    println("2:${Thread.currentThread().name}") // ② 标记2
}

从 ① 和 ② 的输出结果来看,代码的确间隔 1 秒钟执行,从代码顺序上看,delay(1000) 怎么看都是阻塞了线程 1 秒钟,类似 Java 中的 Thread.sleep(1000) ,这看起来不就是阻塞执行了么?

看到这里,相信不少读者已经迷糊了,Kotlin 协程不是非阻塞的么?上面代码看起来是阻塞的啊。

何为阻塞

欸,别慌,先搞明白阻塞是什么,阻塞的对象是谁?

阻塞对比现实世界的例子比较好理解,比如去超市购物结账时:

  • 你结账的窗口扫码枪坏了,结不了账(发生阻塞)
  • 等到换个新的扫码枪才能结账(阻塞恢复)

现在看看阻塞的是谁呢?阻塞的就是这个结账窗口(线程),没错,阻塞的是线程。

阻塞不阻塞是针对当前线程来说的,去别的线程执行,自然不会阻塞当前线程,比如上述结账,你可以去其他窗口进行结账。

回过头看上面的代码例子,无论是从代码执行顺序还是代码执行结果上看,都像是阻塞执行,我们看看下面这个例子:

fun test() {
    println("1:${Thread.currentThread().name}") // ① 标记1
    handler.postDelayed({
        println("2:${Thread.currentThread().name}") // ② 标记2          
    }, 1000)
}

Android 开发同学看见 Handler 应该很熟悉了,对比这两个代码示例,从代码执行顺序还是代码执行结果上是否是一样的呢?或许我们应该看看 delay 的源码了

public suspend fun delay(timeMillis: Long) {
    if (timeMillis <= 0) return // don't delay
    return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
        // if timeMillis == Long.MAX_VALUE then just wait forever like awaitCancellation, don't schedule.
        if (timeMillis < Long.MAX_VALUE) {
            cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
        }
    }
}

cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont) 这个操作,和 handler.postDelay() 类似,本质上是设置一个延时的回调,时间到了就调用 cont 的 resume 等方法让协程恢复。

Kotlin 协程的非阻塞体现在哪呢?

上面我们已经知道阻塞的对象是线程,线程是在 CPU 里执行的,所以可以说阻塞的对象也是 CPU,而 Kotlin 协程的非阻塞只是语言层面的,不是操作系统层面的阻塞,说明在协程中调用阻塞的方法,比如 Thread.sleep() 的时候,协程仍然是阻塞的。

Kotlin 协程的非阻塞是与线程阻塞相比较的,比如我们调用 Thread.sleep() 休眠线程达到延迟的效果,协程则可以使用 delay() 挂起同样达到延迟效果。

那挂起函数挂起了啥?

首先我们需要知道,协程在哪里挂起的,针对第一个代码示例,可以看出来,协程在 delay(1000) 处挂起,那它挂起了什么?

我们不妨想一下,在酒店点菜时的挂起,客人点了一份菜单,但是告知现在不能开始做,等半小时后再开始做,这时候我们可以在系统中先录入这份菜单,点击挂起,输入挂起时长保存,等挂起时长到了,系统自动把菜单发给后厨,后厨开始做菜。

上面酒店点菜的示例,和 delay() 的效果类似,都是延迟触发后续的逻辑。在点菜示例中挂起的什么?是菜单还是做菜动作?这里笔者认为是做菜的动作,而菜单认为是挂起点,即从菜单挂起,也从菜单恢复,对应第一个代码示例,既从 delay(1000) 处挂起,也从 delay(1000) 处恢复,挂起的是 delay(1000) 之后要执行的代码,或者说挂起了协程恢复后要执行的代码,即 ② 处代码。

现在还有 suspendCancellableCoroutine 比较神秘,我们看下它的源码:

public suspend inline fun <T> suspendCancellableCoroutine(
    crossinline block: (CancellableContinuation<T>) -> Unit
): T =
    suspendCoroutineUninterceptedOrReturn { uCont ->
        val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE)
        cancellable.initCancellability()
        block(cancellable)
        cancellable.getResult()
    }

suspendCoroutineUninterceptedOrReturn 这个方法的源码是看不到的,它本身就没有源码:

public suspend inline fun <T> suspendCoroutineUninterceptedOrReturn(crossinline block: (Continuation<T>) -> Any?): T {
    contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
    throw NotImplementedError("Implementation of suspendCoroutineUninterceptedOrReturn is intrinsic")
}

它的作用就是帮我们拿到当前协程的 Continuation 实例,不过我通过翻阅 Kotlin 源码(搜索 suspendCoroutineUninterceptedOrReturn 关键字)找到了以下代码,笔者认为下图1中代码就是具体的实现,代码中的注释,正好对应源码中的 blockContinuation:

image-20220530173205836

总结

Kotlin 协程的挂起函数挂起了啥,可以理解为挂起的是挂起函数恢复之后要执行的逻辑。

说明

以上为笔者个人见解,仁者见仁,智者见智,大家可以求同存异,同时笔者水平有限,如有不同见解,欢迎交流。

参考文献

Footnotes

  1. Kotlin-coroutineCodegenUtil.kt