一、概念
- 函数类型:suspend() → Unit
- 挂起:挂起函数挂起的是协程,让协程和线程分离让出线程,等挂起函数执行完返回到协程中,恢复在之前的线程上执行后续代码。
- 本质:暂停当前任务,中途去做其它事情,做完后回来继续。同样是切线程做其他事情,只是做完了可以回来继续做之前的事。
- 作用:和普通函数封装功能一样,挂起函数通常被放到其他线程中执行,能更方便的指定线程而不用担心调用时出现切换问题。
- 限制:挂起函数“挂起恢复”的特性只能在协程环境下实现,因此只能在其它挂起函数或协程中被调用,创建的只能是子协程。
- 原理:协程会被编译成 switch 语句(状态机模式),每个挂起函数都是其中的一个 case 分支,每个 case 依赖前面的 case,这就是协程切换与挂起停止的原理。
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> { internal open class CancellableContinuationImpl<in T>( |
其它系统挂起函数 Suspend Function
resume( ) | 返回到上一个挂起的协程,并从之前的状态中恢复执行。 |
delay( ) | 会延迟协程执行的时间,不会当前阻塞线程,结束后继续执行协程。自带 isActive 检查机制。 |
measureTimeMillis( ) |
三、自定义挂起函数
3.1 用官方挂起函数封装代码
直接使用系统提供的挂起函数来封装代码是非常方便的,所有官方框架中的挂起函数都是可以取消的。
- 什么时候定义:需要做耗时操作的时候(I/O、计算、等待)才会挂起当前的协程。
- 解决了什么:创建者告知这是一个耗时操作,并指定了该协程在后台线程执行,保障了调用者的线程安全。
- suspend 关键字:用来限制该函数只能在协程里调用或者在其他挂起函数里调用,因为“挂起->执行完->切回去”只有在协程中使用才能实现。真正挂起操作靠的是最终调用的那个协程自带的挂起函数。也有提醒“这是一个耗时操作,是挂起函数要在协程中使用”的意思。
- 步骤:直接使用 Kotlin 提供的挂起函数并不方便,一般包装成一个自定义挂起函数方便调用。在直接使用时,需要对 suspendCoroutine() 指定泛型,是 continuation 携带的返回值类型,resume需要传入相同类型。
3.2 对回调函数进行改造 suspendCancellableCoroutine
对于已有的调用了回调的函数可以改造成挂起函数,onSuccess()中调用resume()返回数据,onFailure()中调用resumeWithException()返回异常,调用invokeOnCancellation()在协程被取消的时候才执行的回调。不推荐使用不可取消的suspendCoroutine,除非是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.isActive
cancellableContinuation.isCancelled
cancellableContinuation.isCompleted
}
四、面试相关
①挂起函数不一定挂起协程:当 async() 的返回值 Deferred 已经可以用时,await() 不会挂起协程而是直接返回结果。