kotlin 协成系列文章:
你真的了解kotlin中协程的suspendCoroutine原理吗?
Kotlin Channel系列(一)之读懂Channel每一行源码
kotlin Flow系列之-冷流SafeFlow源码解析之 - Safe在那里?
Kotlin Flow系列之-ChannelFlow源码解析之 -操作符 buffer & fuse & flowOn线程切换
<<关注微信公众号”皮克桃在写代码“学习更多知识>>
文章目录
引言: 在Kotlin协程中,如何让一个suspend 函数挂起?如何让挂起协程恢复?想必使用过协程的同学都非常清楚那就是调用
suspendCoroutine
或者
suspendCancellableCoroutine
。使用了这么久,你真的知道他们是怎么回事吗?.
注:本文源码班基于kotlin 1.7.10
什么是协程
先简要回顾一下什么是协程?我们通过协程最基础的API createCoroutine
可以创建一个协程,然后在调用resume
开启协程,startCoroutine
创建并直接开启协程。像launch
,async
等这些框架API是在基础API上的再次封装,让我们在创建和使用协程时变得更为方便。协程是由一个 suspend
函数创建出来的,我们来看一个最原始的创建协程方式:
//第一步创建一个suspend 函数
val suspendFun : suspend () -> Unit = {
//TODO 这里面写上协程要执行的代码,也被称之为协程体
}
//第二步创建一个协程,并传一个协程执行完成后的回调
val continuation = suspendFun.createCoroutine(object :Continuation<Unit>{
override val context: CoroutineContext
get() = EmptyCoroutineContext
//协程执行完成后,会回调该方法,result代表了协程的结果,如果没有返回值就是Unit,如果协程体里面发生异常
//result里面包含有异常信息
override fun resumeWith(result: Result<Unit>) {
println("协程执行完毕")
}
})
//第三步开启一个协程
continuation.resume(Unit)
被创建出来的协程continuation
到底是一个什么东西呢?通过createCoroutine
源码发现:
@SinceKotlin("1.3")
@Suppress("UNCHECKED_CAST")
public fun <R, T> (suspend R.() -> T).createCoroutine(
receiver: R,
completion: Continuation<T>
): Continuation<Unit> =
SafeContinuation(createCoroutineUnintercepted(receiver, completion).intercepted(), COROUTINE_SUSPENDED)
这里面做了三件事:
第一:createCoroutineUnintercepted(receiver, completion)
创建了一个协程,”Unintercepted“说明了创建出来的这个协程是不被调度器协程(DispatchedContinuation
)所包含或者代理的,这个就是我们真正执行代码的协程,我把它称之为原始协程。
第二:调用原始协程的intercepted()
,该方法会通过我们在创建协程时指定的调度器创建一个DispatchedContinuation
,它里面包含了原始协程和调度器,如果我们没有指定调度器intercepted()
返回原始协程自己。
第三步:创建一个SafeContinuation
,它持有了intercepted()
返回的对象,设置了调度器就是DispatchedContinuation
,没有设置就是原始协程。
原始协程是被createCoroutineUnintercepted
创建出来的,那到底创建出来的是一个什么东西呢?
public actual fun <T> (suspend () -> T).createCoroutineUnintercepted(
completion: Continuation<T>
): Continuation<Unit> {
val probeCompletion = probeCoroutineCreated(completion)
return if (this is BaseContinuationImpl)
create(probeCompletion)
else
createCoroutineFromSuspendFunction(probeCompletion) {
(this as Function1<Continuation<T>, Any?>).invoke(it)
}
}
在createCoroutineUnintercepted
里面this
是该函数的接收者是一个suspend () -> T
,其实就是我们在上面定义的suspned
函数:
val suspendFun : suspend () -> Unit = {
//TODO 这里面写上协程要执行的代码,也被称之为协程体
}
在kotlin协程中,suspend () -> T
函数类型的父类都是SuspendLambda
。而SuspendLambda
又继承至ContinuationImpl
,ContinuationImpl
又继承至BaseContinuationImpl
,BaseContinuationImpl
又实现了Continuation
接口。因此在createCoroutineUnintercepted
会调用suspend () -> T
的create
函数来创建我们的原始协程。create
函数定义在什么地方呢?是父类SuspendLambda
中还是ContinuationImpl
或者还是BaseContinuationImpl
中呢?它里面又是如何实现呢?都不是,craete
函数编译器为我们生成的。当我们在代码定义一个suspend
函数类型的时候(注意是函数类型不是suspend
函数)编译器会为我们生成一个类,该类继承至SuspendLambda
,把我们原本要执行的代码(协程体代码)给放到一个叫invokeSuspend
函数中,并且为该类生成create
函数,在create
函数中new
一个该类的实例对象返。
如果有对这个比较感兴趣的同学可以在IDEA中把kotlin代码编译后的字节码转成java代码查看。
到此我们知道了我们的原始协程原来是一个由kotlin编译器为我们生成的一个继承了SuspendLambda的子类。知道了原始协程为何物后,协程是如何开启的呢?是怎么执行到我们协程体代码里面的呢?在前面说过,编译器会把我们协程体要执行的代码放到生成的类中的invokeSuspend
函数中,因此我们只需要知道什么时候调用invokeSuspend
就行。在上面代码中的第三步调用了SafeContinuation
的resume
函数,SafeContinuation
中会调用DispatchedContinuation
的resumeWith
函数,在DispatchedContinuation
中又会通过调度器去调用原始协程的resumeWith
函数。原始协程的resumeWitht
函数在BaseContinuationImpl
中定义的:
internal abstract class BaseContinuationImpl(
public val completion: Continuation<Any?>?
) : Continuation<Any?>, CoroutineStackFrame, Serializable {
public final override fun resumeWith(result: Result<Any?>) {
var current = this
var param = result
while (true) {
probeCoroutineResumed(current)
with(current) {
val completion = completion!!
val outcome: Result<Any?> =
try {
//在这里调用了编译器生成的类的invokeSuspend,
//invokeSuspend中就是协程体的代码
val outcome = invokeSuspend(param)
if (outcome === COROUTINE_SUSPENDED) return
Result.success(outcome)
} catch (exception: Throwable) {
Result.failure(exception)
}
releaseIntercepted()
if (completion is BaseContinuationImpl) {
current = completion
param = outcome
} else {
completion.resumeWith(outcome)
return
}
}
}
}
xxxxx
}
在BaseContinuationImpl
的resumeWith
中调用了invokeSuspend
,这样就执行到我们自己写的协程体要执行的代码里面了。
回顾了一大堆,是想说明一个事情,不管是协程第一次执行,还是后面协程从挂起函数恢复都要调用我们原始协程的
resumeWith
函数才行。协程内部的执行(invokeSuspend
内部)是一个状态机,每一次调用invokeSuspend
都会给状态机设置一个不同的状态,使其执行invokeSuspend
中不同分支的代码。至于协程状态机的原理不在本文讨论之中,不然就偏题了。
我们什么时候需挂起协程?被挂起的协程什么时候恢复?当我们不能立马返回结果的时候,需要把协程挂起,等结果准备好了后通过调用协程的resume
函数进行恢复。那协程怎样才能挂起呢?答案就是在suspend
函数中返回一个标识(COROUTINE_SUSPENDED
),当协程看到这个标识后就知道协程需要被挂起了,恢复协程的时候需要调用协程的resume
函数,那我么怎么才能在suspend
函数中拿到协程这个对象呢?只有拿到协程这个对象才能调用其resume
函数。说到这里,想必很多同学都知道调用suspendCoroutine
函数啊,对,没错,当我们需要把我们的suspend
函数挂起的,稍后再恢复的时候,我们可以有三种方式:
- suspendCoroutine
- suspendCancellableCoroutine
- suspendCoroutineUninterceptedOrReturn
其中suspendCoroutine
和 suspendCancellableCoroutine
两个内部都是调用了suspendCoroutineUninterceptedOrReturn
来实现:
public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
//直接调用了suspendCoroutineUninterceptedOrReturn
return suspendCoroutineUninterceptedOrReturn {
c: Continuation<T> ->
val safe = SafeContinuation(c.intercepted())
block(safe)
safe.getOrThrow()
}
}
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
的源码的时候,发现无论如何都找不到源码了,很正常,因为suspendCoroutineUninterceptedOrReturn
是有编译器在编译代码的时时候生成的。所以你找不到很正常。因此搞懂suspendCoroutineUninterceptedOrReturn
的前提下再回过头来看另外两个就简单了。
suspendCoroutineUninterceptedOrReturn
佛说世间万物皆有其因果,任何一个东西的诞生都是有其原因的,在日常开发中,我们经常有这样的需求,调用一个函数获得一个想要的数据,有时候这个数据不能立马获得,比如本地缓存没有,需要从网络下载,这时候就需要把协程挂起,等数据回来了后再把协程恢复,如果本地有就直接返回,简而言之就是有可能会挂起,也有可能不会挂起直接返回结果。因此要让一个suspend
函数能满足这种要求,那需要具备两个条件:1.我们需要再suspend
函数中拿到协程对象,用于恢复协程的时候使用,2.suspend
函数的返回值只能是Any
类型,因为挂起的时候返回COROUTINE_SUSPENDED
,不需要挂起的事后返回数据真实的类型。
针对条件一,我们知道每一个增加了suspned
关键字标识的函数在编译后,函数参数中都会多一个Continuation
的参数。这个参数就是我们的原始协程,但问题的关键是我们是在编码阶段的时候需要拿到协程。所以条件一靠我们自己是搞不定的。
针对条件2,虽然比较好满足,在我们定义函数的时候,把返回值改成Any
即可,但是也同样带来一个问题,获得该函数的结果后,我们还需要人为去判断然后转换成我们需要的数据类型,如果真实这样,协程的代码得有多恶心,那我想估计没几个人愿意使用kotlin协程了。
于是这些kotlin的天才们想了一个招,他们说,你不是想在suspend
函数中要拿到协程对象吗?我有啊,我给你,你只要按照我的要求你随便定义一个函数类型的变量,或者重新再写一个非susnpend
函数。
如果是定义一个变量: 那么这个变量的类型必须为一个函数类型`Continuation<T>) -> Any?`,该函数接收一个`Continuation`作为参数,泛型参数`T`代表了真正需要返回真实数据类型,函数返回值类型为`Any`,给这个变量赋值一个`lambda`表达式,把你原本要写在`suspend`函数的代码放在这个`lambda`表达式里面。
如果是定义一个非suspend
函数:那么这个函数的类型同样为Continuation<T>) -> Any?
,你把原来要写在suspend
函数里面的代码放在这个非suspend
函数里面。
上面两种方式,其本质是一样的,都是一个函数类型,定义好后,kotlin说我给你提供一个叫某某
的函数,我这个某某
函数接收一个函数类型为``Continuation) -> Any?的参数,我这个函数的返回值为泛型
T(代表了你要的结果的真实类型)。你在你需要挂起的
suspned中直接调用我们这个
某某函数,把你定义的函数类型变量或者非suspend函数的引用传给我,我在我的
某某`函数中去调用你传进来的函数,把协程对象传过去,再把计算的结果返回给你suspend函数。这样就达到了想要的目的,既能获得协程对象,又能在需要挂起的时候返回挂起表示,不需要挂起的时候返回具体结果。
听起来如果觉得有点抽象,没关系,我们写一段代码演示一下,比如你现在有一个需求,获得一个Int类型数据,如果这个数据之前被计算出来了就直接返回,如果没有就需要重新计算,计算比较耗时,需要把协程挂起,计算完成把结果缓存起来以便下次直接使用,
故事原本是这样的,我们把代码写在suspend
函数中:
suspend fun calculate(): Any?{
//先不要关心cache是一个啥,只需要知道它可以缓存结果就行
var result = cache.get()
//如果没有缓存就需要开一个子线程去计算,让该函数挂起
if(result == null){
thread {
Thread.sleep(10000)
val result = 1 + 2
cache.put(result)
//计算完成了后调用协程的resume函数让协程恢复。并把计算完成的结果交给协程。
//但是问题来了,contination在源码阶段是那拿不到的。
contination.resume(result) //error
}
//返回COROUTINE_SUSPENDED的目的是让协程挂起。
return COROUTINE_SUSPENDED
}else{
//如果有缓存,直接返回
return result
}
}
虽然calculate函数返回值可以是真实的数据也可以是挂起标识,但是我们拿不到协程对象啊,于是乎我们按照kotlin的要求来,整一个变量,于是你又开始写:
val calculateFun : (Continuation<Int>) -> Any? = {
contination ->
var result = cache.get()
if(result == null){
thread {
Thread.sleep(10000)
val result = 1 + 2
cache.put(result)
contination.resume(result)
}
COROUTINE_SUSPENDED
}else{
result
}
}
然后kotlin给你提供了一个某某
函数,这个函数叫suspendCoroutineUninterceptedOrReturn
: