协程

协程

协程是什么?

  • 协程是可以由程序自行控制挂起、恢复的程序。
  • 协程可以用来实现多任务的协作执行。
  • 协程可以用来解决异步任务控制流的灵活转移。

协程的作用?

  • 协程可以让异步代码同步化。
  • 协程可以降低异步程序的设计复杂度。
  • 挂起和恢复可以控制执行流程的转移。
  • 异步逻辑可以用同步代码的形式写出。
  • 同步代码比异步代码更加灵活,更容易实现复杂业务。

线程和协程

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 则是创建出来的协程的载体,receiver;suspend函数会传给该实例作为协程的实际执行体。

@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的时候才会挂起

协程的线程调度

协程改造的异步程序

原文地址:https://www.cnblogs.com/chen-ying/p/13227027.html