序言
- 你可能对Coroutines一无所知,需要一个入门引导;
- 也可能只是用过,但不了解为什么用,它到底是什么;
- 又或者是对Coroutines有一定了解,但想要探索更全面的设计思想 和 底层原理;
本文希望开启Coroutines的探索大门:从入门引导开始(本文内容),到最佳实践,再深入到原理实现分析,希望做一个较为全面的Coroutines系列讲解(后续)。
希望与处于各个阶段的你一同探索,更加深入的了解Coroutines,以便在工作中更得心应手的使用它来提升工作效率,也能从它的设计中获得一些启发😄。
本文简介
本文将进行Coroutines的重点知识全局引(po)导(bing),源码分析点到为止. 主要讲:Coroutines的设计由来、本质、核心概念、基本用法. 希望能够帮助大家开启揭开Coroutines的神秘面纱之旅.
至于其中的**工作最佳实践、原理&源码分析**,在文章中涉及到的地方会留有PlaceHolder,会计划在后续的文章中进行深入探讨.
为什么使用Coroutines🤔?
一来就塞个东西给你,说:“用它,好用,我教你”. 这样的方式,会让人感到很难受,即便是勉强接受,心里也会嘀咕:凭什么?
其实,就是想问:你这个东西它能给我解决什么问题?
解决了什么问题(Core competence)?
Google I/O 怎么说(at 5min:14s): Coroutines simplify async code by replacing callbacks.
在异步编程把令人头疼(🤯)的Callbacks去掉,就是Coroutines核心想解决的问题。
就这么简单的一件事儿?对,核心就这么看起来“简单”的一件事儿,但痛点绝对是足够了... 那么问题来了,告诉我为什么是痛点?and Coroutines是怎么解决这个问题的?
磨刀不误砍柴工,既然是解决异步编程的问题,不妨先简单回顾一下 同步编程 和 异步编程 是怎么工作的:
- Sync Code: 同步执行等待结果,代码从上往下按照顺序执行,符合人类的认知(串行),简单易懂.
- Happy code style😊.
- Async Code: 通过Callback实现异步线程的执行结果回调,当回调嵌套变多 + 方法变多时,代码可读性 和 异步编程的迭代和管理难度 就会指数上升(痛点就来了,仔细品).
- Struggling code smell🤯.
这是从同步\异步编程的维度上,来看我们平常解决问题的两种代码形式. 我们每天都跟它们打交道,可以说是非常熟悉了.
现在请花几分钟,仔细回顾一下你的编程历史,想想下面的两个问题:
-
当我们有同步执行需求的时候,我们是否是:乐于 并且 善于 写Sync Code(Happy top-down code style😊) ;
-
当我们有异步执行需求的时候,我们是否是:无奈 并且 别扭的 写Async Code(Struggling callback code style🤯);
千呼万唤始出来,我们现在可以来看Coroutines 是怎么 replace callbacks了. (聪明伶俐的你可能已经想到了💡).
- 解决方案💡: 用快乐的同步编程写法(Happy code style),来解决难受的异步编程问题(Struggling code style).
-
Q1: networkRequest是异步执行的吗?
- 是的. 异步执行,不阻塞当前执行的主线程. 当Request执行完成之后结果返回,主线程继续执行.
-
Q2: 没有了callback,有什么魔法🪄?
- 很简单,就一个关键字: suspend🪄. 那suspend又是什么? 见后文核心概念: Suspend
附加对比案例(必看):
如果你觉得写callbacks成本也不大?嗯,那可能被callbacks毒打的还不够,需要丢个王炸出来💣.
无可厚非,每一次新的变革到来之前,我们都会习惯性将旧的规则当作信仰,直到先驱者为他们打开新大陆.
想一想:当Callbacks方法变多 + 嵌套层数变多的时候,耳熟能详的“回调地狱”就出来了👇:
- 回调地狱版,必看🤯: Callback hell(🤯) killer —— Coroutines😊 .
小结: 替换Callback后带给我们什么好处呢?
-
异步编程成本变低了: 用快乐的同步编程写法(Happy top-down code style),来解决难受的异步编程问题(Struggling callback code style).
-
冗余的代码量变少了: 减少了一堆由于callbacks 带来的冗余代码,阅读起来更容易理解.
-
由于callbacks的可读性复杂导致的人工代码问题可能性会减少.****
-
多人协作的持续迭代的产品项目里,代码可读性永远是第一名.
-
Coroutines优势(More than that)?
Coroutines核心是替换callbacks,起于此但不止于此(more than replacing callbacks). 为了解决好这个问题,它还衍生了有很多优势特性:一切都为让开发者能够写异步编程更舒适.
下面来简单介绍它的优势特性:
Non-blocking
上一节已经说到:用同步非阻塞的代码方式,解决异步编程问题;可在suspend点挂起,且不会阻塞当前线程的执行,这是它的最大卖点(核心优势).
- 举例时刻🌰: 上面的例子很深刻了,就再不多举例了.
-
问题时刻🤔: 协程suspend如何做到非阻塞式的异步编程,有什么新的技术吗🪄?
- 回答:哪有什么岁月静好,只是有人替你负重前行. 具体请见下文: Suspend
Structured Concurrency
关键目标:并发任务可管理,不是发出去就算了。
什么是结构化并发模式?
- 如果用一句话概括,那就是:即使是进行并发的操作,也要保证控制流路径的单一入口和单一出口。程序可以产生多个控制流(多个协程)来实现并发,但是所有的并发路径在出口时都应该处于完成 (或取消) 状态,并合并到一起。
Coroutine在语言级别遵循结构化并发编程模式,内置协程任务作用域(CoroutineScope),统一管理(取消\异常处理)Scope内运行的协程,降低并发编程的成本. 具体功能体现在:
- 协程启动: 协程作用域负责执行和启动协程(子协程)。
- 协程管理:协程作用域会等待所有子协程完成才会标记为完成运行。子协程的生命周期依附于其父协程作用域的生命周期。
- 取消协程: 如果协程执行出现问题 或者 用户想要撤销协程任务操作,可以通过作用域取消其下的所有运行的协程(包括子协程)。
- 举例时刻🌰:
- 见下文:
- 如何启动一个协程
- 如何取消正在执行的协程
- 如何捕获协程抛出的异常
- 见下文:
- 问题时刻🤔:Coroutines是如何实现并发结构化编程的?
- 回答: 通过定义CoroutineScope + CoroutineContext来完成协程的追踪 和 生命周期统一管理. 具体解析见下文 CoroutineScope + CoroutineContext.
- 问题时刻🤔:深入了解关于Structured Concurrency的编程设计思想?
Lightweight
Kotlin的协程是语言层级的构造 和 资源分配(可看作一种形式的控制流),并不涉及操作系统侧的资源分配. 所以,在使用上对比线程来说是比较轻量的(你创建协程的时候,心里负担远没有线程那么大).
- 举例时刻🌰:
val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
fun main() = coroutineScope.launch {
repeat(100_100) { //重复启动大量的协程
launch { //coroutine, like a runable code block
delay(5000L) //suspend point 1
print("suspend point 1 resumed")
delay(5000L) //suspend point 2
print("suspend point 2 resumed")
}
}
}
你可以创建十万个协程来执行你的任务,但是你的程序却并不会崩溃 (如果是线程,你可以试试看).
- 问题时刻🤔: 那么Kotlin协程能够提高线程执行的效率吗?或者换句话说,能够提升吞吐量吗?
- 回答:并不能,Kotlin协程本质是语言层面的优化,轻量指的是对使用者来说的轻量(见下文: Suspend ). 但Kotlin协程本质上还是运行在线程当中.
Official extensions Support
Jetpack官方对各种库都支持了丰富的Coroutines的extensions工具,在Android体系下开发协程简单,易上手;
- 举例时刻🌰1:
- Arthitecture Component对Coroutines的支持:viewModelScope、lifeCycleScope、liveData等.
- 举例时刻🌰2:
- AndroidX库对Coroutines的支持:Room、WorkManager、Retrofit等.
- 问题时刻🤔: 这官方会实时更新和维护吗?
-
官方正在大力推广Kotlin和Coroutines,应用层代码大都要用Kotlin和Coroutines来写. 所以,基本上不用担心不会维护.
-
Coroutines 本质是什么🤔?
上面解释我们为什么使用Coroutines。但是从代码库扒下来一看,那么的复杂,各种官方推销的魔法🪄概念五花八门,神乎其神。感觉像是被蒙上了一层神秘的面纱。
从这节开始,我们来慢慢揭开它的面纱,看看它的真面貌。
Kotlin 官网 怎么说?
It is conceptually similar to a thread, in the sense that it takes a block of code to run that works concurrently with the rest of the code.
Coroutines can be thought of as light-weight threads, but there is a number of important differences that make their real-life usage very different from threads.
概念类似线程,轻量级的线程(个人觉得这样有点不太恰当,可能会误导大家). 经过上面的一些简单例子说明,我们或多或少也发现,协程是语言层面的一套概念,底层其实也还是要依赖线程来作为执行环境.
Android官网 怎么说?
A coroutine is a concurrency design pattern that you can use on Android to simplify code that executes asynchronously.
一种并发设计模式,简化异步编程. 这里强调的是并发设计模式,Kotlin并没有引入新的底层技术点,而是一种并发的设计模式的实现. (个人非常支持这种说法,所以Kotlin还得是要靠Google来发扬光大😄)
我们应该怎么理解?
官方说的总感觉有点模糊,似懂非懂。
Coroutines到底本质是什么,我们可以分两部分来看:视觉认知 和 底层本质.
- 首先,从视觉认知上,看看Coroutines外表长什么样?
协程视觉认知就是launch作用域下的一整块代码,跟Runnable非常相像. 这点我非常赞同Google Coroutines推广者 Manuel Vivo 在 协程的介绍 视频里(at 7min: 26s) 对 Coroutines 的描述:
Coroutines is a runnable block code with superpower. Takes a block of code run in a thread.
这里的superpower指的是挂起(suspend). 因为有了它,才实现了在suspend方法处实现挂起. 才赋予Coroutines通过 同步非阻塞的代码 解决 异步并发编程问题 的核心能力.
- 其次,从底层本质上,再看Coroutines到底是什么?
一句话描述:协程是kotlin官方推出的一个语言级别的轻量&高级线程池封装库,用来简化开发者异步编程的成本,使得并发编程易读、易写、易管理 . (请记住,默念十遍)
其实跟React编程思想原理差不多:基于某种设计模式,将线程封装起来并提供简单易用的API给开发人员使用。换句话说,Kotlin的协程就是另外一种风格的RxJava(但比RxJava用起来方便些)。
Coroutines的核心概念♨️?
如上所述,Coroutines是一套语言级别的高级线程池封装库,内部有非常复杂的设计. 但是究其根源,核心的概念有三:Suspend(最核心)、CoroutineScope、CoroutineContext.
下面,我们来看下它们分别起到什么作用:
Suspend
Suspend 是 Coroutines 解决 核心问题(replace callbacks)的 核心武器。
顾名思义,suspend的意思是挂起(也有翻译成暂停的). 官方一点的说法,它具体包括两个方面:
- 挂起(suspend): 遇到挂起点(suspend function)的时候,把执行的协程上下文挂起,以便线程可以去执行别的协程任务;
- 恢复(resume): 当挂起点(suspend function)执行完成之后,再恢复该协程上下文并执行剩余代码;
划重点: Suspend挂起(阻塞)的是协程,恢复的也是协程. 所以,它并不阻塞线程的执行(是有一些前提的,继续看).
Q1: Suspend是如何实现这种魔法🪄?
上文有说到:Coroutines的本质是一种并发设计模式,是一个语言级别支持的标准高级 线程 封装库. 既然是语言级别的封装,那就是新瓶装旧酒.
其实,并不是真的不需要Callbacks,而是 Kotlin 在语言层面帮我们实现了, 开发者不需要写Callbacks了。如下示意图👇:
对应Kotlin实现的(编译器生成的)代码比较复杂:Suspend的魔法🪄揭秘 —— Show me code (实在看不了)
简化之后保留核心的代码,大致长这样:
//编译器给我们生成了Continuaction这个类,并且作为方法参数.
//此处应该有:“啊哈“时刻.
fun loadData(continuation: Continuation<Any?>) {
//continuation式回调
val continuation = continuation ?: object : ContinuationImpl {
// result保存当前结果
val result: Object
// label指示状态机下一个状态
val label: Int
// resume方法将恢复挂起协程的执行
// 被内部的调度器调用,执行协程代码
fun invokeSuspend(Object $result) {
this.result = con.result
this.label = con.label
loadData(this)
}
}
//有限状态机
when(continuation.label) {
0 -> {
//set flag to next suspend point state
completion.label = 1
//execute current suspend point code
//传入了completion作为当该方法执行完成之后的回调
networkRequest(completion)
}
1 -> {
//执行最后一段方法
show(data)
}
else -> {
throw IllegalStateException(...)
}
}
}
到这里,不禁感慨一下:哪有什么岁月静好,只是有人替你负重前行.
划重点:从上面的反编译例子我们可以得出,一个调用了N个suspend function的方法,被suspend point分割成 N + 1个有限状态机;因此,suspend的恢复/取消也就只能在suspend point的方法粒度.
灵魂追问🤔: 完整的Suspend代码的生成 和 实现原理是怎样的?
-
编译器遵循CPS(Continuation - passing - style)的模式进行转换,CPS的本质是:有限状态机 + Continuation. 我会理解成:优雅版Callback.
- 具体源码分析,敬请期待:Kotlin Coroutines under the hood — Suspend(WIP)
Q2: 那什么时候应该使用Suspend呢?
记住,当一个方法被Suspend标记的时候,表示 开发者 在告诉 Kotlin编译器:这是一个耗时的方法,请帮我把它转变成callbacks的执行方式.
仔细品一品这句话会发现:并没有涉及到线程的切换,Kotlin编译器也不会帮我们做线程的切换,它仅仅是把同步代码 转换成了 CPS式 的回调代码 (比我们写的要优雅一些)!
所以,请稳住别浪,Suspend不是永恒的魔法🪄.
那为什么官网说Suspend 是 Main-Safety的,不阻塞UI线程的执行?
其实官网也没有说错,只是大家对Main-Safety理解的角度不一样. 在官方的协程体系里,Suspend是一个通用约定规范,主要体现在两点:
- 对于编译器: 这是一个可挂起的方法,需要把同步代码 转换 生成CPS模式的回调代码.
- 对于开发者: 这是一个可能耗时的方法,需要调用Kotlin提供的协程调度器,并且要自己主动切换到异步线程里执行.
所以,不是所有方法都要声明成suspend,只有耗时方法 或者 IO方法 才需要声明;同时,声明Suspend的方法如果耗时,要负责切换到异步线程里执行,否则该阻塞还是会阻塞(no zuo no die) .
灵魂拷问🤔 : 为什么官方不能直接帮我们生成线程切换的代码?
个人猜测:
- 编译器并不能准确的知道业务方要在哪个suspend点做线程切换调度,切换的策略配置是什么;这是业务要求很灵活的地方,其实不好固化(不然Java也不会暴露Thread让大家自己调用了);
- 不能确定的事儿,不适合交给机器去做,交给能确定的人去做比较好(否则程序员也没有价值了);
CoroutineScope
上面讲Structured Concurrency的时候其实已经讲到,CoroutineScope其实是结构化并发编程模式下在协程里的实现。理解了结构化并发编程,也就理解了CoroutineScope.
同样顾名思义,CoroutineScope就是:在 Kotlin 语言级别标准库层面给Coroutine设定了一个边界,或者说影响范围. 规定Coroutine只能在Scope里面产生 和 执行,以此达到统一取消,以及处理协程异常 的 结构化并发编程目标.
我们来看一个Scope取消协程的例子🌰:
class CoroutinesUseCases {
val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
val job1: Job? = null
fun launchCoroutine() {
//通过Scope启动一个协程,会得到该协程任务所对应的Job.
job1 = coroutineScope.launch {// New coroutine
loadData()
}
job2 = coroutineScope.launch {// New coroutine
loadData()
}
}
fun cancelCoroutine() {
//取消当前的协程任务,会及联的取消其子协程;如何及联,见下文[CoroutineContext]
//注意⚠️:对CoroutineScope没有影响,对该Scope下启动的兄弟Job2也没有影响.
job1?.cancel()
job2?.cancel()
//通过CoroutineScope,取消所有的子协程; 具体原因,见下文[CoroutineContext]
coroutineScope.cancel()
}
}
Q1:CoroutineScope如何做到管理内部的协程?
先看一下CoroutineScope的接口定义:仅包含一个协程上下文的参数.
public interface CoroutineScope {
/**
* The context of this scope.
* Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope.
* Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages.
*
* By convention, should contain an instance of a [job][Job] to enforce structured concurrency.
*/
public val coroutineContext: CoroutineContext
}
So,CoroutineScope就这?如此简单?
- 从设计上讲,"挺简单".
- CoroutineScope是一个结构化并发编程规范的具象化. 它主要包含接口约束 + 协程执行上下文. 通过协程的上下文,找到Scope下所有执行的协程,完成管理协程的目标.
- 从实现上讲,"不简单".
- 下图是实现CoroutineScope的类. 我们可以看到所有类型的协程 都实现了该接口规范.有没有一点熟悉?类似ViewGroup 和 View的关系. 协程本身其实就是一个CoroutineScope!
- 下图是实现CoroutineScope的类. 我们可以看到所有类型的协程 都实现了该接口规范.有没有一点熟悉?类似ViewGroup 和 View的关系. 协程本身其实就是一个CoroutineScope!
其实我们会发现,实际上CoroutineScope它确实什么事情也没做,具体负责协程的调度 和 取消实现是里面的CoroutineContext. 而CoroutineScope的存在的意义是它所定义的接口约束和规范.
CoroutineContext
顾名思义,CoroutineContext其实是:一个存储协程执行所需的一系列上下文环境信息的集合. 这些上下文信息主要包括下面四个(不止这些),而且它们都是CoroutineContext:
- Job:控制当前协程的执行生命周期:开始、取消、join.
- CoroutineDispatcher: 负责调度分配协程在哪个线程中执行.
- CoroutineName:协程的名字,用来Debuging.
- CoroutineExceptionHandler:用来处理协程抛出的异常.
我们来看一个使用例子🌰:及联取消.
val coroutineScope = CoroutineScope(Dispatchers.Main
+ Job()
+ CoroutineName("job"))
val scopedJob: Job? = null
fun startCoroutines() {
//启动一个协程,获得当前协程的Job执行上下文.
scopedJob = coroutineScope.launch {
//case1: 新起一个内部子Scope,通过plus的操作符 "继承" 外部scope的context.
CoroutineScope(this.coroutineContext + Dispatchers.IO).launch {
//do something
}
//case2: 新起一个内部子协程,launch方法内部自动继承当前Scope的context.
launch {
//do something
}
}
//再启动一个新的协程
coroutineScope.launch {
//case3: 新起一个内部子协程,launch方法内部自动继承当前Scope的context.
launch {
//do something
}
}
}
fun cancelAllCoroutines() {
//方式一:通过外层的Coroutine产生的Job来完成及联取消, case1、case2会被取消.
// 因为内部两个协程均关联外层的coroutineContext.
scopedJob?.cancel()
//方式二:通过外层Scope取消,case1、case2、case3都会被取消.
coroutineScope.cancel()
}
我们再看一个异常的案例:异常传播 .
val coroutineScope = CoroutineScope(Dispatchers.IO)
suspend fun main() {
val job = coroutineScope.launch {
launch {
println("开始执行第一个协程")
//do something
delay(5000L)
println("这段代码得不到执行,因为下面的Coroutine抛出来的异常将会取消Scope里所有的协程")
}
launch {
println("开始执行第二个协程")
//do something
delay(5000L)
println("这段代码得不到执行,因为下面的Coroutine抛出来的异常将会取消Scope里所有的协程")
}
}
coroutineScope.launch {
print("开始执行第三个协程")
//do something
delay(3000L)
print("第三个协程执行完成,遇到异常了!😡")
throw IllegalStateException("i am crashed, and nobody catch me!")
}
job.join()
}
背后的故事: 如果某个Child异常,则会将异常先传播到Parent,然后Parent取消其他兄弟协程,然后再取消自己,然后再往父协程传播,达到结构化并发的 及联取消 目的.
- 在协程里,这个机制这叫做:Exception Propagation.
- 核心代码:
- 动态图解:
具体代码分析,敬请期待:Kotlin Coroutines Solutions — Cancelation & Exception Handle(WIP)
下文还有其他通过Scope协作处理协程异常 和 取消的例子🌰:
- 见下文:
- 如何捕获协程抛出的异常
- 如何取消正在执行的协程
Q1:CoroutineContext是如何设计使得它能够追踪内部的协程执行?
我们来看一下CoroutineContext的接口设计:它提供context之间的 +-操作,两个context可以合并 或者 相减,同时提供get查询操作. 这种能够combine合并的特性,官方称之为indexed set(set 和 map的混合体). 这也正是它的强大和精髓之处.
public interface CoroutineContext {
//返回一个CombinedContext
public operator fun plus(context: CoroutineContext): CoroutineContext
public fun minusKey(key: Key<*>): CoroutineContext
public operator fun <E : Element> get(key: Key<E>): E?
public interface Element : CoroutineContext {
public val key: Key<*>
}
}
//看倒这里,就有我们熟悉的链表的影子了.
internal class CombinedContext(
private val left: CoroutineContext,
private val element: Element
) : CoroutineContext, Serializable {
}
本节开头所说的Job\CoroutineDispatchers\CoroutineName\CoroutineExceptionHandler都是继承自Element(也即CoroutineContext).
正因如此的设计,才使得CoroutineContext元素能够累加(同层协程不同元素累加、跨层协程的元素合并). 简单理解可以认为是形成了一个链表,只需要得到表头,就能够找到所有的元素,对他们进行操作.
至此,再回头再看看上面的 及联取消代码,我们至少也可以从设计上解释 及联取消 是怎么做到了:
scopedJob = coroutineScope.launch {
//case1: 新起一个内部子Scope,通过plus的操作符 继承外部scope的context.
CoroutineScope(this.coroutineContext + Dispatchers.IO).launch {
//do something
}
}
- 设计解释:
- Coroutine是CoroutineScope,Job是coroutineContext.
- 外层Coroutine的Job取消 = 该Coroutine Scope取消.
- 而内部的Coroutine(Scope)关联了外部的coroutineContext (通过plus操作符,进行Combine).
- 所以Coroutine Scope取消则会将所有的内部关联的Coroutine取消.
问题时刻🤔: 异常传播、协程调度也都是CoroutineContext的实现,他们的具体代码实现原理是什么?
Coroutines的基本用例🌰?
本节仅举一些非常基本的用例,作为入门理解;在实际开发中,会有更加复杂的使用场景 和 处理方案,里面会涉及更多的机制 和 原理. 这些内容会在 ****[最佳实践(待续)] ****和 [原理分析(待续)] 作为进阶学习进行探讨
Q1: 如何启动一个Coroutines?
Kotlin官方提供了以下两种方式来启动协程:
- launch(推荐): 启动一个新的协程而不将结果返回给调用方,任何被视为“一劳永逸”(one shot)的工作都可以使用 launch 来启动。即,无结果预期的启动协程.
- async: 启动一个新的协程并返回Deferred对象(类似Java的Future),在需要获取结果的地方,调用await的挂起函数等待结果返回。即,有结果预期的启动协程.
注意⚠️: async和launch在处理异常Exception的机制不一样:
- launch启动的协程,内部异常发生会直接在launch代码块抛出,你可以直接try catch;
- async启动的协程,内部抛出异常不会在async代码块抛出(意味着你try catch不了async),直到调用await的时候才会抛出.
因此,使用async可能会有一些“静默异常”你无法追踪到,不会出现在崩溃指标中,也不会在logcat中输出. 因此,一般推荐使用launch. 具体原因见: 如何捕获协程抛出的异常
我们来简单看一下用例:
- launch: 多个launch并行执行
//谨记:所有的协程启动,都需要在CoroutineScope中完成.
val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
fun loadDataWithLaunch() {
//启动一个新的协程,不会阻塞当前代码,继续往下执行
coroutineScope.launch {
loadData1()
}
//立马启动一个新的协程,不需要等待上面协程执行完成.
coroutineScope.launch {
loadData2()
}
}
- async: 多个async并行执行
//谨记:所有的协程启动,都需要在CoroutineScope中完成.
val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
suspend fun loadDataWithAsync() {
val deferredOne = coroutineScope.async {
val result = loadData1()
result
}
val deferredTwo = async {
val result = loadData2()
result
}
//block to get result. exception, if happened, will be throwed at here.
deferredOne.await()
deferredTwo.await()
}
Q2: 如何取消正在执行的协程?
注意⚠️: 取消的粒度是协程内部调用的suspend function. 也即,最快也要等到当前执行的suspend完成后,才能在resume的时候检测取消.
通过CoroutineScope来启动协程,所有在Scope下的协程都将统一收到Scope的管理. 遵循结构化并发编程的范式下,可以统一取消内部所有的协程任务.
val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
var scopedJob: Job? = null
fun onCreate() {
scopedJob = coroutineScope.launch {
//type1:新起一个协程,使用scope的线程调度器
launch {
//doSomething
suspendFunction1()
suspendFunction2()
}
//type2: 新起一个协程,并切换线程调度器
launch(Dispatchers.IO) {
//doSomething
suspendFunction1()
suspendFunction2()
}
//type3: 新起一个协程,并且切换线程调度器,并且要挂起等待执行结果
withContext(Dispatchers.IO) {
//doSomething
suspendFunction1()
suspendFunction2()
}
//type4: 非suspend function中新起一个Scope,
// 并且切换线程调度器到Dispatchers.IO.
nonSuspendFunction(coroutineContext)
}
}
fun nonSuspendFunction(coroutinesContext: CoroutineContext) {
CoroutineScope(coroutineContext + Dispatchers.IO) {
//doSomething
suspendFunction1()
suspendFunction2()
}
}
fun onDestroy() {
//页面销毁的时候调用cancel,则上述4种type下执行的协程任务都会在
//suspend的point点被取消.
scopedJob?.cancel()
}
关于更多协程取消的最佳使用案例,敬请期待:Kotlin Coroutines Solutions — Cancelation & Exception Handle(WIP)
Q3: 如何捕获协程抛出的异常?
在协程里捕获异常,有两种方式:
- Try catch(推荐) : 一般性正常思维,在执行的代码中使用try catch的方式.
-
CoroutineExceptionHandler: 通过往Scope中注入Handler处理那些没有try catch的异常。平常业务编程中尽量不要自己使用,除非你对异常传播机制比较了解,且有明确的场景要用到.
- 用途举例:协程的稳定性指标监控.
我们来看一下使用案例吧🌰:
- try catch:
val coroutineScope = CoroutineScope(Dispatchers.IO)
fun onCreate() {
//case1(推荐)协程内部的局部代码try catch.
coroutineScope.launch {
//do something
try {
networkRequest()
} catch(t: Throwable) {
//handle exception here.
}
//do another thing
showResult()
}
//case2:对launch协程进行整体try catch.
try {
coroutineScope.launch {
//do something
delay(5000L)
throw IllegalStateException("i am crashed, and nobody catch me!")
}
} catch(t: Throwable) {
//catch exception throwed from launch coroutine
}
//case3:对asyn的协程进行try catch
//错误方式:❌
try {
coroutineScope.async {
//do something
delay(5000L)
throw IllegalStateException("i am crashed, and nobody catch me!")
}
} catch (t: Throwable) {
//我们无法在这里捕捉到异常.
}
//正确方式:✅
val defferedResult = coroutineScope.async {
//do something
delay(5000L)
throw IllegalStateException("i am crashed, and nobody catch me!")
}
try {
//在await执行的时候才会抛出来,在此前都会被存放在Coroutine内部中.
defferedResult.await()
} catch(t: Throwable) {
//我们在这里便能够捕捉到async里抛出的异常.
}
}
- CoroutineExceptionHandler:
//当我们往Scope中设置CoroutineExceptionHandler之后,就能捕获未处理的异常.
val coroutineScope = CoroutineScope(Dispatchers.IO
+ CoroutineExceptionHandler { coroutineContext, throwable ->
print("we get an exception, thread will still running, message is: ${throwable.message}")
})
fun onCreate() {
scopedJob = coroutineScope.launch {
//do something
delay(5000L)
throw IllegalStateException("i am crashed!")
}
}
关于更多协程异常处理的最佳使用案例:
- 推荐阅读:Exception in coroutines (非常建议看下这篇官方推广者的Medium文章)
Q4: 如何切换协程的执行线程?
在协程中,切换线程的方式有几种:launch切换、async切换、withContext切换(推荐) .
我们来看一下使用例子:
- launch切换: 在协程执行作用域内,通过launch新起一个协程,并切换到其他线程执行.
val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
var scopedJob: Job? = null
fun onCreate() {
scopedJob = coroutineScope.launch {
//切换到IO线程,store db
launch(Dispatchers.IO) {
//doSomething
suspendFunction1()
suspendFunction2()
}
}
}
- async切换: 在协程执行作用域内,通过async新起一个协程,并切换到其他线程执行.
val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
var scopedJob: Job? = null
fun onCreate() {
scopedJob = coroutineScope.launch {
//切换到IO线程
val deffered = async(Dispatchers.IO) {
//doSomething
suspendFunction1()
suspendFunction2()
}
try {
//等待执行结果,并catch可能发生的异常
deffered.await()
} catch(t: Throwable) {
//handle exception here.
}
}
}
- withContext切换(推荐) : 在当前协程里,切换到其他线程执行,并且能够挂起等待内部的返回结果. withContext消耗很小,比起launch和async的切换方式高效很多!
- withContext是一个suspend方法,不阻塞当前线程.
val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
var scopedJob: Job? = null
fun onCreate() {
scopedJob = coroutineScope.launch {
//切换到IO线程,并等待执行结果.
val result = withContext(Dispatchers.IO) {
val result = loadData()
return result
}
show(result)
}
}
后续相关系列
最佳实践
-
coroutines solutions
-
coroutines on Android