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

相关文章

hdu1171(母函数或多重背包)

题意:把物品分成两份,使得价值最接近 可以用背包,或者是母函数来解,母函数(1 + x^v+x^2v+.....+x^num*v)(1 + x^v+x^2v+.....+x^num*v)(1 + x^v+x^2v+.....+x^num*v) 其中指数为价值,每一项的数目为(该物品数+1)个 代码如下: #include<iostream>#include<algorithm>

C++操作符重载实例(独立函数)

C++操作符重载实例,我们把坐标值CVector的加法进行重载,计算c3=c1+c2时,也就是计算x3=x1+x2,y3=y1+y2,今天我们以独立函数的方式重载操作符+(加号),以下是C++代码: c1802.cpp源代码: D:\YcjWork\CppTour>vim c1802.cpp #include <iostream>using namespace std;/*** 以独立函数

函数式编程思想

我们经常会用到各种各样的编程思想,例如面向过程、面向对象。不过笔者在该博客简单介绍一下函数式编程思想. 如果对函数式编程思想进行概括,就是f(x) = na(x) , y=uf(x)…至于其他的编程思想,可能是y=a(x)+b(x)+c(x)…,也有可能是y=f(x)=f(x)/a + f(x)/b+f(x)/c… 面向过程的指令式编程 面向过程,简单理解就是y=a(x)+b(x)+c(x)

利用matlab bar函数绘制较为复杂的柱状图,并在图中进行适当标注

示例代码和结果如下:小疑问:如何自动选择合适的坐标位置对柱状图的数值大小进行标注?😂 clear; close all;x = 1:3;aa=[28.6321521955954 26.2453660695847 21.69102348512086.93747104431360 6.25442246899816 3.342835958564245.51365061796319 4.87

OpenCV结构分析与形状描述符(11)椭圆拟合函数fitEllipse()的使用

操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C++11 算法描述 围绕一组2D点拟合一个椭圆。 该函数计算出一个椭圆,该椭圆在最小二乘意义上最好地拟合一组2D点。它返回一个内切椭圆的旋转矩形。使用了由[90]描述的第一个算法。开发者应该注意,由于数据点靠近包含的 Mat 元素的边界,返回的椭圆/旋转矩形数据

Unity3D 运动之Move函数和translate

CharacterController.Move 移动 function Move (motion : Vector3) : CollisionFlags Description描述 A more complex move function taking absolute movement deltas. 一个更加复杂的运动函数,每次都绝对运动。 Attempts to

AutoGen Function Call 函数调用解析(一)

目录 一、AutoGen Function Call 1.1 register_for_llm 注册调用 1.2 register_for_execution 注册执行 1.3 三种注册方法 1.3.1 函数定义和注册分开 1.3.2 定义函数时注册 1.3.3  register_function 函数注册 二、实例 本文主要对 AutoGen Function Call

✨机器学习笔记(二)—— 线性回归、代价函数、梯度下降

1️⃣线性回归(linear regression) f w , b ( x ) = w x + b f_{w,b}(x) = wx + b fw,b​(x)=wx+b 🎈A linear regression model predicting house prices: 如图是机器学习通过监督学习运用线性回归模型来预测房价的例子,当房屋大小为1250 f e e t 2 feet^

使用协程实现高并发的I/O处理

文章目录 1. 协程简介1.1 什么是协程?1.2 协程的特点1.3 Python 中的协程 2. 协程的基本概念2.1 事件循环2.2 协程函数2.3 Future 对象 3. 使用协程实现高并发的 I/O 处理3.1 网络请求3.2 文件读写 4. 实际应用场景4.1 网络爬虫4.2 文件处理 5. 性能分析5.1 上下文切换开销5.2 I/O 等待时间 6. 最佳实践6.1 使用 as

JavaSE(十三)——函数式编程(Lambda表达式、方法引用、Stream流)

函数式编程 函数式编程 是 Java 8 引入的一个重要特性,它允许开发者以函数作为一等公民(first-class citizens)的方式编程,即函数可以作为参数传递给其他函数,也可以作为返回值。 这极大地提高了代码的可读性、可维护性和复用性。函数式编程的核心概念包括高阶函数、Lambda 表达式、函数式接口、流(Streams)和 Optional 类等。 函数式编程的核心是Lambda