【安卓】Kotlin协程实现原理:Suspend&CoroutineContext

Kotlin协程实现原理:Suspend&CoroutineContext

午后一小憩发布于 2020-11-19

【安卓】Kotlin协程实现原理:Suspend&CoroutineContext

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

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

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

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

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

协程是什么

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

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

协程是一种并发设计模式,您可以在 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), [email protected][dispatcher = DefaultDispatcher]], CoroutineName(aa)

[CoroutineName(aa), [email protected][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: 每日一算法,由浅入深,欢迎加入一起共勉。

程序员android前端kotlincoroutine

阅读 1.1k发布于 2020-11-19

本作品系原创,采用《署名-非商业性使用-禁止演绎 4.0 国际》许可协议


大前端成长之路

大前端开发干货!公众号:Android补给站

avatar

午后一小憩

微信公众号:Android补给站

2.9k 声望

66 粉丝

0 条评论

得票时间

avatar

午后一小憩

微信公众号:Android补给站

2.9k 声望

66 粉丝

宣传栏

【安卓】Kotlin协程实现原理:Suspend&CoroutineContext

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

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

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

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

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

协程是什么

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

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

协程是一种并发设计模式,您可以在 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), [email protected][dispatcher = DefaultDispatcher]], CoroutineName(aa)

[CoroutineName(aa), [email protected][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: 每日一算法,由浅入深,欢迎加入一起共勉。

以上是 【安卓】Kotlin协程实现原理:Suspend&amp;CoroutineContext 的全部内容, 来源链接: utcz.com/a/105898.html

回到顶部