36

Kotlin协程实现原理:Suspend&CoroutineContext

 3 years ago
source link: https://www.rousetime.com/2020/11/11/Kotlin协程实现原理-Suspend-CoroutineContext/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

今天我们来聊聊 Kotlin 的协程 Coroutine

如果你还没有接触过协程,推荐你先阅读这篇入门级文章 What? 你还不知道Kotlin Coroutine?

如果你已经接触过协程,相信你都有过以下几个疑问:

  1. 协程到底是个什么东西?
  2. 协程的 suspend 有什么作用,工作原理是怎样的?
  3. 协程中的一些关键名称(例如: JobCoroutineDispatcherCoroutineContextCoroutineScope )它们之间到底是怎么样的关系?
  4. 协程的所谓非阻塞式挂起与恢复又是什么?
  5. 协程的内部实现原理是怎么样的?

接下来的一些文章试着来分析一下这些疑问,也欢迎大家一起加入来讨论。

协程是什么

这个疑问很简单,只要你不是野路子接触协程的,都应该能够知道。因为官方文档中已经明确给出了定义。

下面来看下官方的原话(也是这篇文章最具有底气的一段话)。

协程是一种并发设计模式,您可以在 Android 平台上使用它来简化异步执行的代码。

敲黑板划重点:协程是一种并发的设计模式。

所以并不是一些人所说的什么线程的另一种表现。虽然协程的内部也使用到了线程。但它更大的作用是它的设计思想。将我们传统的 Callback 回调方式进行消除。将异步编程趋近于同步对齐。

解释了这么多,最后我们还是直接点,来看下它的优点

  1. 轻量:您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。
  2. 内存泄露更少:使用结构化并发机制在一个作用域内执行多个操作。
  3. 内置取消支持:取消功能会自动通过正在运行的协程层次结构传播。
  4. Jetpack集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发。

suspend

suspend 是协程的关键字,每一个被 suspend 修饰的方法都必须在另一个 suspend 函数或者 Coroutine 协程程序中进行调用。

第一次看到这个定义不知道你们是否有疑问,反正小憩我是很疑惑,为什么 suspend 修饰的方法需要有这个限制呢?不加为什么就不可以,它的作用到底是什么?

当然,如果你有关注我之前的文章,应该就会有所了解,因为在 重温Retrofit源码,笑看协程实现 这篇文章中我已经有简单的提及。

这里涉及到一种机制俗称 CPS(Continuation-Passing-Style) 。每一个 suspend 修饰的方法或者 lambda 表达式都会在代码调用的时候为其额外添加 Continuation 类型的参数。

@GET("/v2/news")
suspend fun newsGet(@QueryMap params: Map<String, String>): NewsResponse

上面这段代码经过 CPS 转换之后真正的面目是这样的

@GET("/v2/news")
fun newsGet(@QueryMap params: Map<String, String>, c: Continuation<NewsResponse>): Any?

经过转换之后,原有的返回类型 NewsResponse 被添加到新增的 Continutation 参数中,同时返回了 Any? 类型。这里可能会有所疑问?返回类型都变了,结果不就出错了吗?

其实不是, Any?Kotlin 中比较特殊,它可以代表任意类型。

suspend 函数被协程挂起时,它会返回一个特殊的标识 COROUTINE_SUSPENDED ,而它本质就是一个 Any ;当协程不挂起进行执行时,它将返回执行的结果或者引发的异常。这样为了让这两种情况的返回都支持,所以使用了 Kotlin 独有的 Any? 类型。

返回值搞明白了,现在来说说这个 Continutation 参数。

首先来看下 Continutation 的源码

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>)
}

context 是协程的上下文,它更多时候是 CombinedContext 类型,类似于协程的集合,这个后续会详情说明。

resumeWith 是用来唤醒挂起的协程。前面已经说过协程在执行的过程中,为了防止阻塞使用了挂起的特性,一旦协程内部的逻辑执行完毕之后,就是通过该方法来唤起协程。让它在之前挂起的位置继续执行下去。

所以每一个被 suspend 修饰的函数都会获取上层的 Continutation ,并将其作为参数传递给自己。既然是从上层传递过来的,那么 Continutation 是由谁创建的呢?

其实也不难猜到, Continutation 就是与协程创建的时候一起被创建的。

GlobalScope.launch { 
             
}

launch 的时候就已经创建了 Continutation 对象,并且启动了协程。所以在它里面进行挂起的协程传递的参数都是这个对象。

简单的理解就是协程使用 resumeWith 替换传统的 callback ,每一个协程程序的创建都会伴随 Continutation 的存在,同时协程创建的时候都会自动回调一次 ContinutationresumeWith 方法,以便让协程开始执行。

CoroutineContext

协程的上下文,它包含用户定义的一些数据集合,这些数据与协程密切相关。它类似于 map 集合,可以通过 key 来获取不同类型的数据。同时 CoroutineContext 的灵活性很强,如果其需要改变只需使用当前的 CoroutineContext 来创建一个新的 CoroutineContext 即可。

来看下 CoroutineContext 的定义

public interface CoroutineContext {
    /**
     * Returns the element with the given [key] from this context or `null`.
     */
    public operator fun <E : Element> get(key: Key<E>): E?
 
    /**
     * Accumulates entries of this context starting with [initial] value and applying [operation]
     * from left to right to current accumulator value and each element of this context.
     */
    public fun <R> fold(initial: R, operation: (R, Element) -> R): R
 
    /**
     * Returns a context containing elements from this context and elements from  other [context].
     * The elements from this context with the same key as in the other one are dropped.
     */
    public operator fun plus(context: CoroutineContext): CoroutineContext = ...
 
    /**
     * Returns a context containing elements from this context, but without an element with
     * the specified [key].
     */
    public fun minusKey(key: Key<*>): CoroutineContext
 
    /**
     * Key for the elements of [CoroutineContext]. [E] is a type of element with this key.
     */
    public interface Key<E : Element>

    /**
     * An element of the [CoroutineContext]. An element of the coroutine context is a singleton context by itself.
     */
    public interface Element : CoroutineContext {..}
}

每一个 CoroutineContext 都有它唯一的一个 Key 其中的类型是 Element ,我们可以通过对应的 Key 来获取对应的具体对象。说的有点抽象我们直接通过例子来了解。

var context = Job() + Dispatchers.IO + CoroutineName("aa")
LogUtils.d("$context, ${context[CoroutineName]}")
context = context.minusKey(Job)
LogUtils.d("$context")
// 输出
[JobImpl{Active}@158b42c, CoroutineName(aa), LimitingDispatcher@aeb0f27[dispatcher = DefaultDispatcher]], CoroutineName(aa)
[CoroutineName(aa), LimitingDispatcher@aeb0f27[dispatcher = DefaultDispatcher]]

JobDispatchersCoroutineName 都实现了 Element 接口。

如果需要结合不同的 CoroutineContext 可以直接通过 + 拼接,本质就是使用了 plus 方法。

public operator fun plus(context: CoroutineContext): CoroutineContext =
    if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation
        context.fold(this) { acc, element ->
            val removed = acc.minusKey(element.key)
            if (removed === EmptyCoroutineContext) element else {
                // make sure interceptor is always last in the context (and thus is fast to get when present)
                val interceptor = removed[ContinuationInterceptor]
                if (interceptor == null) CombinedContext(removed, element) else {
                    val left = removed.minusKey(ContinuationInterceptor)
                    if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
                        CombinedContext(CombinedContext(left, element), interceptor)
                }
            }
        }

plus 的实现逻辑是将两个拼接的 CoroutineContext 封装到 CombinedContext 中组成一个拼接链,同时每次都将 ContinuationInterceptor 添加到拼接链的最尾部.

那么 CombinedContext 又是什么呢?

internal class CombinedContext(
    private val left: CoroutineContext,
    private val element: Element
) : CoroutineContext, Serializable {
 
    override fun <E : Element> get(key: Key<E>): E? {
        var cur = this
        while (true) {
            cur.element[key]?.let { return it }
            val next = cur.left
            if (next is CombinedContext) {
                cur = next
            } else {
                return next[key]
            }
        }
    }
    ...
}

注意看它的两个参数,我们直接拿上面的例子来分析

Job() + Dispatchers.IO
(Job, Dispatchers.IO)

Job 对应于 leftDispatchers.IO 对应 element 。如果再拼接一层 CoroutineName(aa) 就是这样的

((Job, Dispatchers.IO),CoroutineName)

功能类似与链表,但不同的是你能够拿到上一个与你相连的 整体 内容。与之对应的就是 minusKey 方法,从集合中移除对应 KeyCoroutineContext 实例。

有了这个基础,我们再看它的 get 方法就很清晰了。先从 element 中去取,没有再从之前的 left 中取。

那么这个 Key 到底是什么呢?我们来看下 CoroutineName

public data class CoroutineName(
    /**
     * User-defined coroutine name.
     */
    val name: String
) : AbstractCoroutineContextElement(CoroutineName) {
    /**
     * Key for [CoroutineName] instance in the coroutine context.
     */
    public companion object Key : CoroutineContext.Key<CoroutineName>
 
    /**
     * Returns a string representation of the object.
     */
    override fun toString(): String = "CoroutineName($name)"
}

很简单它的 Key 就是 CoroutineContext.Key<CoroutineName> ,当然这样还不够,需要继续结合对于的 operator get 方法,所以我们再来看下 Elementget 方法

public override operator fun <E : Element> get(key: Key<E>): E? =
    @Suppress("UNCHECKED_CAST")
    if (this.key == key) this as E else null

这里使用到了 Kotlinoperator 操作符重载的特性。那么下面的代码就是等效的。

context.get(CoroutineName)
context[CoroutineName]

所以我们就可以直接通过类似于 Map 的方式来获取整个协程中 CoroutineContext 集合中对应 KeyCoroutineContext 实例。

本篇文章主要介绍了 suspend 的工作原理与 CoroutineContext 的内部结构。希望对学习协程的伙伴们能够有所帮助,敬请期待后续的协程分析。

项目

android_startup : 提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。不仅支持 Jetpack App Startup 的全部功能,还提供额外的同步与异步等待、线程控制与多进程支持等功能。

AwesomeGithub : 基于 Github 客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用 Kotlin 语言进行开发,项目架构是基于 Jetpack&DataBindingMVVM ;项目中使用了 ArouterRetrofitCoroutineGlideDaggerHilt 等流行开源技术。

flutter_github : 基于 Flutter 的跨平台版本 Github 客户端,与 AwesomeGithub 相对应。

android-api-analysis : 结合详细的 Demo 来全面解析 Android 相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。

daily_algorithm : 每日一算法,由浅入深,欢迎加入一起共勉。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK