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

相关文章

MySQL常用字符串函数示例和场景介绍

《MySQL常用字符串函数示例和场景介绍》MySQL提供了丰富的字符串函数帮助我们高效地对字符串进行处理、转换和分析,本文我将全面且深入地介绍MySQL常用的字符串函数,并结合具体示例和场景,帮你熟练... 目录一、字符串函数概述1.1 字符串函数的作用1.2 字符串函数分类二、字符串长度与统计函数2.1

python使用try函数详解

《python使用try函数详解》Pythontry语句用于异常处理,支持捕获特定/多种异常、else/final子句确保资源释放,结合with语句自动清理,可自定义异常及嵌套结构,灵活应对错误场景... 目录try 函数的基本语法捕获特定异常捕获多个异常使用 else 子句使用 finally 子句捕获所

postgresql使用UUID函数的方法

《postgresql使用UUID函数的方法》本文给大家介绍postgresql使用UUID函数的方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录PostgreSQL有两种生成uuid的方法。可以先通过sql查看是否已安装扩展函数,和可以安装的扩展函数

MySQL字符串常用函数详解

《MySQL字符串常用函数详解》本文给大家介绍MySQL字符串常用函数,本文结合实例代码给大家介绍的非常详细,对大家学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录mysql字符串常用函数一、获取二、大小写转换三、拼接四、截取五、比较、反转、替换六、去空白、填充MySQL字符串常用函数一、

Kotlin Map映射转换问题小结

《KotlinMap映射转换问题小结》文章介绍了Kotlin集合转换的多种方法,包括map(一对一转换)、mapIndexed(带索引)、mapNotNull(过滤null)、mapKeys/map... 目录Kotlin 集合转换:map、mapIndexed、mapNotNull、mapKeys、map

C++中assign函数的使用

《C++中assign函数的使用》在C++标准模板库中,std::list等容器都提供了assign成员函数,它比操作符更灵活,支持多种初始化方式,下面就来介绍一下assign的用法,具有一定的参考价... 目录​1.assign的基本功能​​语法​2. 具体用法示例​​​(1) 填充n个相同值​​(2)

MySql基本查询之表的增删查改+聚合函数案例详解

《MySql基本查询之表的增删查改+聚合函数案例详解》本文详解SQL的CURD操作INSERT用于数据插入(单行/多行及冲突处理),SELECT实现数据检索(列选择、条件过滤、排序分页),UPDATE... 目录一、Create1.1 单行数据 + 全列插入1.2 多行数据 + 指定列插入1.3 插入否则更

PostgreSQL中rank()窗口函数实用指南与示例

《PostgreSQL中rank()窗口函数实用指南与示例》在数据分析和数据库管理中,经常需要对数据进行排名操作,PostgreSQL提供了强大的窗口函数rank(),可以方便地对结果集中的行进行排名... 目录一、rank()函数简介二、基础示例:部门内员工薪资排名示例数据排名查询三、高级应用示例1. 每

全面掌握 SQL 中的 DATEDIFF函数及用法最佳实践

《全面掌握SQL中的DATEDIFF函数及用法最佳实践》本文解析DATEDIFF在不同数据库中的差异,强调其边界计算原理,探讨应用场景及陷阱,推荐根据需求选择TIMESTAMPDIFF或inte... 目录1. 核心概念:DATEDIFF 究竟在计算什么?2. 主流数据库中的 DATEDIFF 实现2.1

MySQL中的LENGTH()函数用法详解与实例分析

《MySQL中的LENGTH()函数用法详解与实例分析》MySQLLENGTH()函数用于计算字符串的字节长度,区别于CHAR_LENGTH()的字符长度,适用于多字节字符集(如UTF-8)的数据验证... 目录1. LENGTH()函数的基本语法2. LENGTH()函数的返回值2.1 示例1:计算字符串