Kotlin 协程 - 挂起函数 Suspend Function

2023-10-09 15:59

本文主要是介绍Kotlin 协程 - 挂起函数 Suspend Function,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、概念

  • 函数类型:suspend() → Unit
  • 本质:暂停当前任务,中途去做其它事情,做完后回来继续。
  • 作用:和普通函数封装功能一样,挂起函数通常被放到其他线程中执行,能更方便的指定线程而不用担心调用时出现切换问题。
  • 限制:挂起函数“挂起恢复”的特性只能在协程环境下实现,因此只能在其它挂起函数或协程中被调用,创建的只能是子协程。

1.1 挂起恢复的过程

①挂起函数挂起的是父协程。

  • 此时被挂起的父协程:代码不会继续往下执行(即写在挂起函数下面的那些,而挂起函数上面的代码没执行完的话,挂起函数指定在相同线程执行会等挂起函数执行完再执行,指定在其它线程则继续并发执行)。
  • 此时被挂起的父协程所在的线程:不会阻塞而是脱离当前任务去执行其他任务或无事可干(就是它原本受系统调度的样子,回收或再利用)。

②父协程被挂起后,挂起函数在它指定的线程中执行自己的代码。

③挂起函数执行完后,携带结果恢复到父协程中,父协程从之前的进度继续往下执行。

1.2 非阻塞式

  • 单线程是阻塞式的,当前任务没做完就不会执行后面的代码,多线程是切到别的线程执行就不会阻塞之前的线程。
  • 对比其它基于 Java 的多线程解决方案,协程借助 Kotlin 的语言优势以及“挂起恢复”的特性消除了回调嵌套,即原先串行写的代码现在并行来写(让串行嵌套的异步代码像同步那样并行编写),逻辑直观并消除模板代码(Java切线程会回调里嵌套回调),降低了多线程异步之间协作任务的操作难度(调用起来不用考虑任务是执行在哪个线程)。

1.3 suspend 关键字

  • 编码阶段:用来限制该函数只能在协程或者在其他挂起函数里调用,因为“挂起恢复”只有在协程中使用才能实现。由于挂起函数通常被拿来包装子线程耗时任务,因此也有该提醒的意思。只有函数体中调用了由协程库提供的挂起函数,才会使用到 continuation 参数,suspend才不会多余。
  • 编译阶段:由 suspend 修饰的函数在被编译后会增加一个 Continuation 延续体形参,因此在函数体中就可以通过延续体对象来恢复父协程或取消当前协程,这就是CPS(Continuation Passing Style)延续体传递方式。详见下方原理。

二、延续体 Continuation

是一个接口,类似于一个挂起函数执行完携带结果恢复的 callback回调,是对父协程中该挂起函数之后的代码封装(恢复后需要执行的代码就拿到了挂起函数中的结果)。子接口 CancellableContinuation 表示可以取消的延续体。他们的实现类 ContinuationImpl 和 CancellableContinuationImpl 都是 intel 修饰的,意味着 Continuation 延续体对象不是自行创建获得,而是挂起函数通过 suspendCoroutineUninterceptedOrReturn() 获得。

Continuation

resumeWith( )

为了方便调用可以使用下面两个扩展函数,不然需要手动将值或异常包装为Result对象使用。

public fun resumeWith(result: Result<T>)

携带结果恢复被挂起的父协程,结果被封装在 Result 对象中,可以是:Result .success(传值)、Result .failure(传异常)。

Continuation.resume( )

Continuation.resumeWithException( )

public inline fun <T> Continuation<T>.resume(value: T): Unit = resumeWith(Result.success(value))

携带值恢复。

public inline fun <T> Continuation<T>.resumeWithException(exception: Throwable): Unit = resumeWith(Result.failure(exception))

携带异常恢复。

2.1 组成结构 

BaseContinuationImpl基本实现类,实现了resumeWith()以控制延续体状态机的执行流程,定义了invokeSuspend()抽象函数由Kotlin编译器自动实现,函数体的实现是协程中的执行代码块内容。
ContinuationImpl继承自BaseContinuationImpl,增加了intercepted()拦截器功能,实现线程调度等。
SuspendLambda继承自ContinuationImpl,是对协程代码块中代码的封装。
编译生成的匿名对象

Kotlin在编译时,每个协程都会生成SuspendLambda的匿名子类并创建其对象(协程嵌套就有多个),有两层含义:

①对协程代码(我们写的Lambda)的封装,以挂起函数为分割点将代码分为多个部分填充到invokeSuspend()函数中(具体在switch维护的状态机)从而实现SuspendLambda。

②实现Continuation所以同时又是当前协程的延续体,具有恢复到上一个状态中的能力。

2.2 底层实现原理

Kotlin编译后会为每个协程生成一个 SuspendLambda 的匿名实现类对象(协程嵌套协程会生成多个):

  • 非阻塞式挂起:对于我们在协程中写的Lambda代码(即block),会以挂起函数为分割点,将代码分为多个部分(状态)填充到 invokeSuspend() 函数中。当协程开始执行时会首先调用一次 invokeSuspend() 触发初始化,当执行到挂起函数的时候,判断挂起成功会返回COROUTINE_SUSPEND标志,导致 invokeSuspend() 函数 return 停止执行。
  • 状态机:在使用 lebal 标记嵌套的代码块方便内部 switch() 跳出,每一层嵌套都是一个挂起函数中的内容(内层是上一个,外层是下一个),会将状态值+1,最里层是一个 switch() 语句,当每次恢复调用进来会执行对应状态值的代码(判断Result携带的结果是异常就抛出,正常会跳出对应代码块也就是挂起函数执行完就跳出这层,然后执行内层的上一个挂起函数内容),由此保证了各个状态是按顺序执行(用同步的方式写出异步代码)。
  • Continuation传递:由于实现了Continuation,每次判断挂起成功 return 的时候,SuspendLambda 都会将自己作为延续体传递过去。避免了每个挂起函数都需要创建延续体对象。
  • 协程的恢复:当挂起函数执行完会调用延续体的 resumeWith() 携带结果(值或异常)恢复,而该函数中又会调用 invokeSuspend(),根据状态机的状态值执行下一个状态的代码。

2.3 ​​​​挂起点

从语法上说是调用一个挂起函数的位置,真正的挂起点是该挂起函数中调用了协程库提供的原始挂起函数中return了一个枚举值COROUTINE_SUSPENDED作为标记,使得当前函数退出执行从而挂起协程。

2.4 可取消的延续体 CancellableContinuation

public interface CancellableContinuation<in T> : Continuation<T> {
    //检查延续体状态
    public val isActive: Boolean
    public val isCancelled: Boolean
    public val isCompleted: Boolean
    //使用可选的异常来结束掉这个延续体,成功返回true。
    public fun cancel(cause: Throwable? = null): Boolean
    //当延续体被取消或抛出异常时调用,一般用来释放资源。
    public fun invokeOnCancellation(handler: CompletionHandler)
}

internal open class CancellableContinuationImpl<in T>(
    final override val delegate: Continuation<T>,
    resumeMode: Int
) : DispatchedTask<T>(resumeMode), CancellableContinuation<T>, CoroutineStackFrame {
    internal fun getResult(): Any? {
        //父协程成功挂起,就返回COROUTINE_SUSPENDED
        if (trySuspend()) {... return COROUTINE_SUSPENDED }
        //否则就返回异常或值
        if (isReusable) { ... }    //如果父协程已恢复(或自己是伪挂起函数)
        if (state is CompletedExceptionally) { ... }    //如果父协程异常
        if (resumeMode.isCancellableMode) { ... }    //如果父协程已取消
        return getSuccessfulResult(state)    //如果父协程成功计算出值
    }
}

其它系统挂起函数 Suspend Function

resume( )返回到上一个挂起的协程,并从之前的状态中恢复执行。

delay( )

会延迟协程执行的时间,不会当前阻塞线程,结束后继续执行协程。自带 isActive 检查机制。
measureTimeMillis( )

三、自定义挂起函数

3.1 用官方挂起函数封装代码

直接使用系统提供的挂起函数来封装代码是非常方便的,所有官方框架中的挂起函数都是可以取消的。

  • 什么时候定义:需要做耗时操作的时候(I/O、计算、等待)才会挂起当前的协程。
  • 解决了什么:创建者告知这是一个耗时操作,并指定了该协程在后台线程执行,保障了调用者的线程安全。
  • suspend 关键字:用来限制该函数只能在协程里调用或者在其他挂起函数里调用,因为“挂起->执行完->切回去”只有在协程中使用才能实现。真正挂起操作靠的是最终调用的那个协程自带的挂起函数。也有提醒“这是一个耗时操作,是挂起函数要在协程中使用”的意思。
  • 步骤:直接使用 Kotlin 提供的挂起函数并不方便,一般包装成一个自定义挂起函数方便调用。在直接使用时,需要对 suspendCoroutine() 指定泛型,是 continuation 携带的返回值类型,resume需要传入相同类型。

3.2 对回调函数进行改造 suspendCancellableCoroutine

对于已有的调用了回调的函数可以改造成挂起函数,onSuccess()中调用resume()返回数据,onFailure()中调用resumeWithException()返回异常。不推荐使用不可取消的suspendCoroutine。

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()
    }
//改造前
fun request() {getData(object: Callback {override fun onSuccess(str: String) { ... }override fun onFailure(exception: Throwable) { ... }})
}
//改造后
suspend fun requestWithSuspend(): String = suspendCancelableCoroutine { cancelableContinuation ->getData(object: Callback) {//携带结果恢复override fun onSuccess(str: String) { cancelableContinuation.resume(str) }//携带异常恢复override fun onFailure(exception: Throwable) { cancelableContinuation.resumeWithException(exception) }}//取消cancellableContinuation.cancel()//被取消时的回调,用来释放资源cancellableContinuation.invokeOnCancellation { }//检查延续体状态cancellableContinuation.isActivecancellableContinuation.isCancelledcancellableContinuation.isCompleted
}

四、面试相关

挂起函数不一定挂起协程:当 async() 的返回值 Deferred 已经可以用时,await() 不会挂起协程而是直接返回结果。

这篇关于Kotlin 协程 - 挂起函数 Suspend Function的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/173979

相关文章

Python中help()和dir()函数的使用

《Python中help()和dir()函数的使用》我们经常需要查看某个对象(如模块、类、函数等)的属性和方法,Python提供了两个内置函数help()和dir(),它们可以帮助我们快速了解代... 目录1. 引言2. help() 函数2.1 作用2.2 使用方法2.3 示例(1) 查看内置函数的帮助(

C++ 函数 strftime 和时间格式示例详解

《C++函数strftime和时间格式示例详解》strftime是C/C++标准库中用于格式化日期和时间的函数,定义在ctime头文件中,它将tm结构体中的时间信息转换为指定格式的字符串,是处理... 目录C++ 函数 strftipythonme 详解一、函数原型二、功能描述三、格式字符串说明四、返回值五

Python中bisect_left 函数实现高效插入与有序列表管理

《Python中bisect_left函数实现高效插入与有序列表管理》Python的bisect_left函数通过二分查找高效定位有序列表插入位置,与bisect_right的区别在于处理重复元素时... 目录一、bisect_left 基本介绍1.1 函数定义1.2 核心功能二、bisect_left 与

Android学习总结之Java和kotlin区别超详细分析

《Android学习总结之Java和kotlin区别超详细分析》Java和Kotlin都是用于Android开发的编程语言,它们各自具有独特的特点和优势,:本文主要介绍Android学习总结之Ja... 目录一、空安全机制真题 1:Kotlin 如何解决 Java 的 NullPointerExceptio

java中BigDecimal里面的subtract函数介绍及实现方法

《java中BigDecimal里面的subtract函数介绍及实现方法》在Java中实现减法操作需要根据数据类型选择不同方法,主要分为数值型减法和字符串减法两种场景,本文给大家介绍java中BigD... 目录Java中BigDecimal里面的subtract函数的意思?一、数值型减法(高精度计算)1.

C++/类与对象/默认成员函数@构造函数的用法

《C++/类与对象/默认成员函数@构造函数的用法》:本文主要介绍C++/类与对象/默认成员函数@构造函数的用法,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录名词概念默认成员函数构造函数概念函数特征显示构造函数隐式构造函数总结名词概念默认构造函数:不用传参就可以

C++类和对象之默认成员函数的使用解读

《C++类和对象之默认成员函数的使用解读》:本文主要介绍C++类和对象之默认成员函数的使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、默认成员函数有哪些二、各默认成员函数详解默认构造函数析构函数拷贝构造函数拷贝赋值运算符三、默认成员函数的注意事项总结一

Python函数返回多个值的多种方法小结

《Python函数返回多个值的多种方法小结》在Python中,函数通常用于封装一段代码,使其可以重复调用,有时,我们希望一个函数能够返回多个值,Python提供了几种不同的方法来实现这一点,需要的朋友... 目录一、使用元组(Tuple):二、使用列表(list)三、使用字典(Dictionary)四、 使

Kotlin Compose Button 实现长按监听并实现动画效果(完整代码)

《KotlinComposeButton实现长按监听并实现动画效果(完整代码)》想要实现长按按钮开始录音,松开发送的功能,因此为了实现这些功能就需要自己写一个Button来解决问题,下面小编给大... 目录Button 实现原理1. Surface 的作用(关键)2. InteractionSource3.

PyTorch中cdist和sum函数使用示例详解

《PyTorch中cdist和sum函数使用示例详解》torch.cdist是PyTorch中用于计算**两个张量之间的成对距离(pairwisedistance)**的函数,常用于点云处理、图神经网... 目录基本语法输出示例1. 简单的 2D 欧几里得距离2. 批量形式(3D Tensor)3. 使用不