

Kotlin协程实现原理:Suspend&CoroutineContext
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?
如果你已经接触过协程,相信你都有过以下几个疑问:
- 协程到底是个什么东西?
-
协程的
suspend
有什么作用,工作原理是怎样的? -
协程中的一些关键名称(例如:
Job
、Coroutine
、Dispatcher
、CoroutineContext
与CoroutineScope
)它们之间到底是怎么样的关系? - 协程的所谓非阻塞式挂起与恢复又是什么?
- 协程的内部实现原理是怎么样的?
接下来的一些文章试着来分析一下这些疑问,也欢迎大家一起加入来讨论。
协程是什么
这个疑问很简单,只要你不是野路子接触协程的,都应该能够知道。因为官方文档中已经明确给出了定义。
下面来看下官方的原话(也是这篇文章最具有底气的一段话)。
协程是一种并发设计模式,您可以在 Android 平台上使用它来简化异步执行的代码。
敲黑板划重点:协程是一种并发的设计模式。
所以并不是一些人所说的什么线程的另一种表现。虽然协程的内部也使用到了线程。但它更大的作用是它的设计思想。将我们传统的 Callback
回调方式进行消除。将异步编程趋近于同步对齐。
解释了这么多,最后我们还是直接点,来看下它的优点
- 轻量:您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。
- 内存泄露更少:使用结构化并发机制在一个作用域内执行多个操作。
- 内置取消支持:取消功能会自动通过正在运行的协程层次结构传播。
- 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
的存在,同时协程创建的时候都会自动回调一次 Continutation
的 resumeWith
方法,以便让协程开始执行。
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]]
Job
、 Dispatchers
与 CoroutineName
都实现了 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
对应于 left
, Dispatchers.IO
对应 element
。如果再拼接一层 CoroutineName(aa)
就是这样的
((Job, Dispatchers.IO),CoroutineName)
功能类似与链表,但不同的是你能够拿到上一个与你相连的 整体
内容。与之对应的就是 minusKey
方法,从集合中移除对应 Key
的 CoroutineContext
实例。
有了这个基础,我们再看它的 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
方法,所以我们再来看下 Element
的 get
方法
public override operator fun <E : Element> get(key: Key<E>): E? = @Suppress("UNCHECKED_CAST") if (this.key == key) this as E else null
这里使用到了 Kotlin
的 operator
操作符重载的特性。那么下面的代码就是等效的。
context.get(CoroutineName) context[CoroutineName]
所以我们就可以直接通过类似于 Map
的方式来获取整个协程中 CoroutineContext
集合中对应 Key
的 CoroutineContext
实例。
本篇文章主要介绍了 suspend
的工作原理与 CoroutineContext
的内部结构。希望对学习协程的伙伴们能够有所帮助,敬请期待后续的协程分析。
项目
android_startup
: 提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。不仅支持 Jetpack App Startup
的全部功能,还提供额外的同步与异步等待、线程控制与多进程支持等功能。
AwesomeGithub
: 基于 Github
客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用 Kotlin
语言进行开发,项目架构是基于 Jetpack&DataBinding
的 MVVM
;项目中使用了 Arouter
、 Retrofit
、 Coroutine
、 Glide
、 Dagger
与 Hilt
等流行开源技术。
flutter_github
: 基于 Flutter
的跨平台版本 Github
客户端,与 AwesomeGithub
相对应。
android-api-analysis
: 结合详细的 Demo
来全面解析 Android
相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。
daily_algorithm : 每日一算法,由浅入深,欢迎加入一起共勉。
Recommend
-
68
最近抽出闲暇,把 kotlinx.coroutines 官方的三份入手指南翻译了一下,挂在了 GitBook ,可以直接去这里查看。不过,文档的内容其实还是比较多的,为了厘清协程的特殊之处,下面我就总结一番。
-
78
Kotlin Coroutines(协程) 完全解析(一),协程简介
-
66
At the heart of Kotlin coroutines is the CoutineContext interface. All the coroutine builder functions like launch and async have the same first parameter, context: CoroutineContext. These coroutine…
-
37
Kotlin Coroutines(协程) 完全解析系列: Kotlin Coroutines(协程) 完全解析(一),协程简介
-
23
今天我们来聊聊 Kotlin 的协程 Coroutine 。 如果你还没有接触过协程,推荐你先阅读这篇入门级文章
-
4
再谈协程之CoroutineContext我能玩一年
-
9
再谈协程之suspend到底挂起了啥
-
9
Binance to Temporarily Suspend ETH & ERC-20 Token Withdrawals
-
10
kotlin的suspend对比csharp的async&await
-
5
你不知道的CoroutineContext:协程上下文大揭秘! 2024.01.06 Rouse
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK