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

相关文章

Android Kotlin 高阶函数详解及其在协程中的应用小结

《AndroidKotlin高阶函数详解及其在协程中的应用小结》高阶函数是Kotlin中的一个重要特性,它能够将函数作为一等公民(First-ClassCitizen),使得代码更加简洁、灵活和可... 目录1. 引言2. 什么是高阶函数?3. 高阶函数的基础用法3.1 传递函数作为参数3.2 Lambda

C++中::SHCreateDirectoryEx函数使用方法

《C++中::SHCreateDirectoryEx函数使用方法》::SHCreateDirectoryEx用于创建多级目录,类似于mkdir-p命令,本文主要介绍了C++中::SHCreateDir... 目录1. 函数原型与依赖项2. 基本使用示例示例 1:创建单层目录示例 2:创建多级目录3. 关键注

C++中函数模板与类模板的简单使用及区别介绍

《C++中函数模板与类模板的简单使用及区别介绍》这篇文章介绍了C++中的模板机制,包括函数模板和类模板的概念、语法和实际应用,函数模板通过类型参数实现泛型操作,而类模板允许创建可处理多种数据类型的类,... 目录一、函数模板定义语法真实示例二、类模板三、关键区别四、注意事项 ‌在C++中,模板是实现泛型编程

kotlin的函数forEach示例详解

《kotlin的函数forEach示例详解》在Kotlin中,forEach是一个高阶函数,用于遍历集合中的每个元素并对其执行指定的操作,它的核心特点是简洁、函数式,适用于需要遍历集合且无需返回值的场... 目录一、基本用法1️⃣ 遍历集合2️⃣ 遍历数组3️⃣ 遍历 Map二、与 for 循环的区别三、高

kotlin中的数据转换方法(示例详解)

《kotlin中的数据转换方法(示例详解)》这篇文章介绍了Kotlin中将数字转换为字符串和字符串转换为数字的多种方法,包括使用`toString()`、字符串模板、格式化字符串、处理可空类型等,同时... 目录1. 直接使用 toString() 方法2. 字符串模板(自动转换)3. 格式化字符串(控制输

C语言字符函数和字符串函数示例详解

《C语言字符函数和字符串函数示例详解》本文详细介绍了C语言中字符分类函数、字符转换函数及字符串操作函数的使用方法,并通过示例代码展示了如何实现这些功能,通过这些内容,读者可以深入理解并掌握C语言中的字... 目录一、字符分类函数二、字符转换函数三、strlen的使用和模拟实现3.1strlen函数3.2st

kotlin中的行为组件及高级用法

《kotlin中的行为组件及高级用法》Jetpack中的四大行为组件:WorkManager、DataBinding、Coroutines和Lifecycle,分别解决了后台任务调度、数据驱动UI、异... 目录WorkManager工作原理最佳实践Data Binding工作原理进阶技巧Coroutine

kotlin中的模块化结构组件及工作原理

《kotlin中的模块化结构组件及工作原理》本文介绍了Kotlin中模块化结构组件,包括ViewModel、LiveData、Room和Navigation的工作原理和基础使用,本文通过实例代码给大家... 目录ViewModel 工作原理LiveData 工作原理Room 工作原理Navigation 工

MySQL中COALESCE函数示例详解

《MySQL中COALESCE函数示例详解》COALESCE是一个功能强大且常用的SQL函数,主要用来处理NULL值和实现灵活的值选择策略,能够使查询逻辑更清晰、简洁,:本文主要介绍MySQL中C... 目录语法示例1. 替换 NULL 值2. 用于字段默认值3. 多列优先级4. 结合聚合函数注意事项总结C

Java8需要知道的4个函数式接口简单教程

《Java8需要知道的4个函数式接口简单教程》:本文主要介绍Java8中引入的函数式接口,包括Consumer、Supplier、Predicate和Function,以及它们的用法和特点,文中... 目录什么是函数是接口?Consumer接口定义核心特点注意事项常见用法1.基本用法2.结合andThen链