自从用了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)
}
}
}
}
至此,挂起的原理就解释完了,想要了解更细可阅读参考文章。