你真的了解kotlin中协程的suspendCoroutine原理吗?

本文详细探讨了kotlin协程中的suspendCoroutine和suspendCancellableCoroutine的原理,包括它们如何挂起和恢复协程、如何处理返回值以及存在的问题。文章解释了为何要使用这两个函数,以及它们与suspendCoroutineUninterceptedOrReturn的关系,并通过代码示例展示了它们的工作机制。此外,还分析了它们存在的问题,如线程环境、异常处理和使用不当导致的异常。最后,介绍了suspendCoroutine的线程安全和线程切换特性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

kotlin 协成系列文章:

你真的了解kotlin中协程的suspendCoroutine原理吗?

Kotlin Channel系列(一)之读懂Channel每一行源码

kotlin Flow系列之-冷流SafeFlow源码解析之 - Safe在那里?

kotlin Flow系列之-SharedFlow源码解析

kotlin Flow系列之-StateFlow源码解析

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又继承至ContinuationImplContinuationImpl又继承至BaseContinuationImplBaseContinuationImpl又实现了Continuation接口。因此在createCoroutineUnintercepted会调用suspend () -> Tcreate函数来创建我们的原始协程。create函数定义在什么地方呢?是父类SuspendLambda中还是ContinuationImpl或者还是BaseContinuationImpl中呢?它里面又是如何实现呢?都不是,craete函数编译器为我们生成的。当我们在代码定义一个suspend函数类型的时候(注意是函数类型不是suspend函数)编译器会为我们生成一个类,该类继承至SuspendLambda,把我们原本要执行的代码(协程体代码)给放到一个叫invokeSuspend函数中,并且为该类生成create函数,在create函数中new一个该类的实例对象返。

如果有对这个比较感兴趣的同学可以在IDEA中把kotlin代码编译后的字节码转成java代码查看。

​ 到此我们知道了我们的原始协程原来是一个由kotlin编译器为我们生成的一个继承了SuspendLambda的子类。知道了原始协程为何物后,协程是如何开启的呢?是怎么执行到我们协程体代码里面的呢?在前面说过,编译器会把我们协程体要执行的代码放到生成的类中的invokeSuspend函数中,因此我们只需要知道什么时候调用invokeSuspend就行。在上面代码中的第三步调用了SafeContinuationresume函数,SafeContinuation中会调用DispatchedContinuationresumeWith函数,在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

}

​ 在BaseContinuationImplresumeWith中调用了invokeSuspend,这样就执行到我们自己写的协程体要执行的代码里面了。

回顾了一大堆,是想说明一个事情,不管是协程第一次执行,还是后面协程从挂起函数恢复都要调用我们原始协程的resumeWith函数才行。协程内部的执行(invokeSuspend内部)是一个状态机,每一次调用invokeSuspend都会给状态机设置一个不同的状态,使其执行invokeSuspend中不同分支的代码。至于协程状态机的原理不在本文讨论之中,不然就偏题了。

​ 我们什么时候需挂起协程?被挂起的协程什么时候恢复?当我们不能立马返回结果的时候,需要把协程挂起,等结果准备好了后通过调用协程的resume函数进行恢复。那协程怎样才能挂起呢?答案就是在suspend函数中返回一个标识(COROUTINE_SUSPENDED),当协程看到这个标识后就知道协程需要被挂起了,恢复协程的时候需要调用协程的resume函数,那我么怎么才能在suspend函数中拿到协程这个对象呢?只有拿到协程这个对象才能调用其resume函数。说到这里,想必很多同学都知道调用suspendCoroutine函数啊,对,没错,当我们需要把我们的suspend函数挂起的,稍后再恢复的时候,我们可以有三种方式:

  1. suspendCoroutine
  2. suspendCancellableCoroutine
  3. suspendCoroutineUninterceptedOrReturn

其中suspendCoroutinesuspendCancellableCoroutine两个内部都是调用了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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

皮克桃在写代码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值