协程
协程
协程是什么?
- 协程是可以由程序自行控制挂起、恢复的程序。
- 协程可以用来实现多任务的协作执行。
- 协程可以用来解决异步任务控制流的灵活转移。
协程的作用?
- 协程可以让异步代码同步化。
- 协程可以降低异步程序的设计复杂度。
- 挂起和恢复可以控制执行流程的转移。
- 异步逻辑可以用同步代码的形式写出。
- 同步代码比异步代码更加灵活,更容易实现复杂业务。
线程和协程
Kotlin协程只是一个“线程框架”?
- 运行在线程上的框架不一定就是“线程框架”,例如所有框架
- 支持线程切换的框架也不一定就是“线程框架”,例如OkHttp
Kotlin协程
-
官方协程框架(框架级别的支持)
Job
调度器
作用域
-
Kotlin标准库(语言级别的支持)
协程上下文
拦截器
挂起函数
协程的分类
按调用栈
- 有栈协程:每个协程会分配单独的调用栈,类似线程的调用栈。可以在任意函数嵌套中挂起,例如Lua Coroutine
- 无栈协程:不会分配单独的调用栈,挂起点状态通过闭包或者对象保存。只能在当前函数中挂起,例如Python Generator
按调用关系
- 对称协程:调度权可以转移给任意协程,协程之间的关系是对等的。
- 非对称协程:调度权只能转移给调用自己的协程,协程存在父子关系。
协程的常见实现
协程:挂起和恢复
-
Python Generator
-
Go routine
每个Go Routine都是并发或者并行执行
无Buffer的channel写时会挂起,直到读取,反之依然
GoRoutine 可以认为是一种有栈对称协程的实现
-
Lua Coroutine
-
async/await
很多语言都支持
可以多层嵌套,但是必须是async function
async/await是一种无栈非对称的协程实现
是目前语言支持最广泛的特性
-
...
协程基本要素
挂起函数
- 挂起函数:以suspend修饰的函数
- 挂起函数只能在其他挂起函数或者协程中调用
- 挂起函数调用时包含了协程“挂起”的语义,挂起函数的调用处称为“挂起点”,刮起的时候主调用流程就挂起了。
- 挂起函数返回时则包含了协程“恢复”的语义,恢复的时候主调用流程就恢复了。
Continuation
有栈协程可以把挂起点的状态保存在栈当中,但是无栈协程的话是会保存在闭包当中,而在Kotlin当中是会通过Continuation保存挂起点的状态。
@SinceKotlin("1.3")
public interface Continuation<in T> {
/**
* The context of the coroutine that corresponds to this continuation.
*/
public val context: CoroutineContext
/**
* Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the
* return value of the last suspension point.
*/
public fun resumeWith(result: Result<T>)
}
@SinceKotlin("1.3")
@InlineOnly
public inline fun <T> Continuation<T>.resume(value: T): Unit =
resumeWith(Result.success(value))
@SinceKotlin("1.3")
@InlineOnly
public inline fun <T> Continuation<T>.resumeWithException(exception: Throwable): Unit =
resumeWith(Result.failure(exception))
这里的话可以看到,挂起和恢复都是伴随着正常结果的返回和异常结果的返回。
其实调用一个挂起函数是需要传入一个Continuation的,只是这是编译器已经完成的事情,而只有挂起函数和协程才会拥有一个Continuation。
挂起函数类型
比如:suspend() -> Unit、suspend(String) -> String
-
将回调转写成挂起函数:
真正的挂起时必须异步调用resume,切换到其他县城resume或者是单线程事件循环异步执行;而如果在suspendCoroutine中直接调用resume也算是没有挂起。
使用suspendCoroutine获取挂起函数的Continuation,而回调成功的分支使用Continuation.resume(value);而回调失败则使用Continuation.resumeWithException(e)
协程的创建
- 首先,协程是一段可执行的程序
- 协程的创建通常需要一个函数 suspend function
- 协程的创建也需要一个API。 createCoroutine、startCoroutine
suspend函数本身执行的时候需要一个Continuation实例在恢复时调用,即此处的参数:completion;返回值Continuation
@SinceKotlin("1.3")
@Suppress("UNCHECKED_CAST")
public fun <T> (suspend () -> T).createCoroutine(
completion: Continuation<T>
): Continuation<Unit> =
SafeContinuation(createCoroutineUnintercepted(completion).intercepted(), COROUTINE_SUSPENDED)
@SinceKotlin("1.3")
@Suppress("UNCHECKED_CAST")
public fun <T> (suspend () -> T).startCoroutine(
completion: Continuation<T>
) {
createCoroutineUnintercepted(completion).intercepted().resume(Unit)
}
协程上下文
- 协程执行过程中需要携带数据
- 索引是CoroutineContext.Key
- 元素是CoroutineContext.Element
拦截器
- 拦截器CoroutineIntereptor是一类协程上下文元素
- 可以对协程上下文所在的协程Continuation进行拦截与篡改。
@SinceKotlin("1.3")
public interface ContinuationInterceptor : CoroutineContext.Element {
public fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T>
//...
}
//标准库中的SuspendLambda就是
@SinceKotlin("1.3")
// Suspension lambdas inherit from this class
internal abstract class SuspendLambda(
public override val arity: Int,
completion: Continuation<Any?>?
) : ContinuationImpl(completion), FunctionBase<Any?>, SuspendFunction {
constructor(arity: Int) : this(arity, null)
public override fun toString(): String =
if (completion == null)
Reflection.renderLambdaToString(this) // this is lambda
else
super.toString() // this is continuation
}
//SuspendLambda对应的就是下面suspend包含的部分
suspend{
a()
}.startCoroutine(...)
//而如果suspend里面有调用了挂起函数,在调用的过程中还会用SafeContinuation包装SuspendLambda
Continuaion的执行
-
SafeContinuation的作用就是确保
-
resume只被调用一次
-
如果在当前线程调用栈上直接调用则不会挂起。
-
-
拦截Continuaion
会用Intercepted先将SuspendLambda包装一次,每次SafeContinuation调用resume的时候会先调用Intercepted返回的Continuation的resume。
- SafeContinuation仅在挂起点的时候出现
- 拦截器在每次恢复或者执行协程体的时候调用
- SuspendLambda是协程函数体
协程挂起恢复要点
- 协程体内的代码都是通过Continuation.resumeWith调用
- 每调用一次lable加1,每一个挂起点对应于一个case分支
- 挂起函数返回COUROUTINE_SUSPENDED的时候才会挂起
协程的线程调度
协程改造的异步程序