安卓开发者在面试中应该怎么回答协程挂起的原理

3,723 阅读3分钟

自从用了kotlin之后,接触了协程这个在Java中没有的概念,不久后就被它的魅力所留住,彻底忘记了RxJava,然而虽然日常你在项目中经常用协程,如果面试的时候面试官问你协程挂起的原理,该怎么回答呢?本文的行文不会像其它文章一样力求结合大量的源码来解释,因为你面试中无法摆出源码,而是试图用最少量的代码和简洁的语言帮你回答这个问题。

首先应该明确,在线程中通常说的是阻塞和唤醒,在协程中通常说挂起和恢复,他们是有区别的。

如果把公交车比作线程,乘客比作执行的逻辑,道路比作CPU调度。那么线程阻塞就是开车中途停了车给一人上了厕所,让出了路,其他人只能在车上等待,这是线程阻塞,等到人回来时才能继续开车,回到道路,这是线程唤醒。公车上导游带着一个团,途中导游突然说要下车去订酒店,于是跟所有人一起下车,导游自己换车去订酒店,其余人在原地等候通知,不久后司机继续开了,有能力去服务其它乘客,这是协程挂起。导游完事之后打电话给团友,等下一趟公车来的时候就上车了,这个车可能是原来那辆也可能是其它的车,这是协程恢复。

总结就是:协程挂起时,没有将所在的线程阻塞,所在线程可以被继续调度。

在协程中挂起操作很简单,调用其它suspend函数即可,那代码层面的原理究竟是什么?

// 建议反编译这段代码结合后面的内容理解
fun test() {
    viewModelScope.launch {
        Log.d("test", "test: enter coroutine")
        withContext(Dispatchers.Default) {
            Log.d("test", "test: enter withContext")
        }
        Log.d("test", "test: after withContext")
    }
}

看看withContext的源码

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T {
    return suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
        // 旧的Context
        val oldContext = uCont.context
        // 构造新Context
        val newContext = oldContext.newCoroutineContext(context)
        ···
        // 处理新Context和旧Context一样的情况
        
        // 构造分发的协程
        val coroutine = DispatchedCoroutine(newContext, uCont)
        // 开启协程
        block.startCoroutineCancellable(coroutine, coroutine)
        // 获取协程结果
        coroutine.getResult()
    }
}

这个uCont就是父协程,用它当参数创建了一个分发的协程,suspendCoroutineUninterceptedOrReturn的返回值是Lambda的最后一句,也就是coroutine.getResult()的结果,这里会判断是否需要挂起,挂起的话会返回一个枚举值COROUTINE_SUSPENDED,所以挂起的逻辑就是看这返回值谁用到了。

答案是launch,launch块是一个协程体,它会被封装成一个Continuation,当执行这个协程体时,会调用resumeWith函数尝试返回结果,但是没走完拿到的是withContext返回的一个COROUTINE_SUSPENDED,于是它直接return了,结束了调用,这就是挂起让出了线程调度。

简单看下代码

// BaseContinuationImpl
public final override fun resumeWith(result: Result<Any?>) {
        var current = this
        var param = result
        while (true) {
            with(current) {
                val completion = completion!!
                val outcome: Result<Any?> =
                    try {
                        val outcome = invokeSuspend(param)
                        // 此处的判断
                        if (outcome === COROUTINE_SUSPENDED) return
                        Result.success(outcome)
                    } catch (exception: Throwable) {
                        Result.failure(exception)
                    }
            }
        }
    }

至此,挂起的原理就解释完了,想要了解更细可阅读参考文章。

讲真,Kotlin 协程的挂起没那么神秘(原理篇) - 掘金 (juejin.cn)