Kotlin 协程之取消与异常处理探索之旅(上)
前言
协程系列文章:
我们知道线程可以被终止,线程里可以抛出异常,类似的协程也会遇到此种情况。本篇将从线程的终止与异常处理分析开始,逐渐引入协程的取消与异常处理。
通过本篇文章,你将了解到:
- 线程的终止
- 线程的异常处理
- 协程的Job 结构
1. 线程的终止
如何终止一个线程
阻塞状态下终止
先看个Demo:
class ThreadDemo {
fun testStop() {
//构造线程
var t1 = thread {
println("thread start")
Thread.sleep(2000)
println("thread end")
}
//1s后中断线程
Thread.sleep(1000)
t1.interrupt()
}
}
fun main(args : Array) {
var threadDemo = ThreadDemo()
threadDemo.testStop()
}
结果如下:
可以看出,"thread end" 没有打印出来,说明线程被成功中断了。
上述Demo里线程能够被中断的本质是:
Thread.sleep(xx)方法会检测中断状态,若是发现发生了中断,则抛出异常。
非阻塞状态下终止
改造一下Demo:
class ThreadDemo {
fun testStop() {
//构造线程
var t1 = thread {
var count = 0
println("thread start")
while (count < 100000000) {
count++
}
println("thread end count:$count")
}
//等待线程运行
Thread.sleep(10)
println("interrupt t1 start")
t1.interrupt()
println("interrupt t1 end")
}
}
运行结果如下:
可以看出,线程启动后,中断线程,而最后线程依然正常运行到结束,说明此时线程并没有被中断。
本质原因:
interrupt() 方法仅仅只是唤醒线程与设置中断标记位。
此种场景下如何终止一个线程呢?我们继续改造一下Demo:
class ThreadDemo {
fun testStop() {
//构造线程
var t1 = thread {
var count = 0
println("thread start")
//检测是否被中断
while (count < 100000000 && !Thread.interrupted()) {
count++
}
println("thread end count:$count")
}
//等待线程运行
Thread.sleep(10)
println("interrupt t1 start")
t1.interrupt()
println("interrupt t1 end")
}
}
对比之前的Demo,仅仅只是添加了中断标记检测:Thread.interrupted()。
该方法返回true表示该线程被中断了,于是我们手动停止计数。
结果如下:
由此可见,线程被成功终止了。
综上所述,如何终止一个线程我们有了结论:
更加深入的分析原理以及两者的结合使用请移步:Java “优雅”地中断线程(实践篇)
2. 线程的异常处理
不论在Java 还是Kotlin里,异常都是可以通过try...catch 捕获。
典型如下:
fun testException() {
try {
1/0
} catch (e : Exception) {
println("e:$e")
}
}
结果:
成功捕获了异常。
改造一下Demo:
fun testException() {
try {
//开启线程
thread {
1/0
}
} catch (e : Exception) {
println("e:$e")
}
}
大家先猜测一下结果,能够捕获异常吗?
接着来看结果:
很遗憾,无法捕获。
根本原因:
异常的捕获是针对当前线程的堆栈。而上述Demo是在main(主)线程里进行捕获,而异常时发生在子线程里。
你可能会说,简单我直接在子线程里进行捕获即可。
fun testException() {
thread {
try {
1/0
} catch (e : Exception) {
println("e:$e")
}
}
}
这么做没毛病,很合理也很刚。
考虑另一种场景:若是主线程想要获取子线程异常的原因,进而做不同的处理。
这时候就引入了:UncaughtExceptionHandler。
继续改造Demo:
fun testException3() {
try {
//开启线程
var t1 = thread(false){
1/0
}
t1.name = "myThread"
//设置
t1.setUncaughtExceptionHandler { t, e ->
println("${t.name} exception:$e")
}
t1.start()
} catch (e : Exception) {
println("e:$e")
}
}
其实就是注册了个回调,当线程发生异常时会调用uncaughtException(xx)方法。
结果如下:
说明成功捕获了异常。
3. 协程的Job 结构
Job 基础
Job 的创建
在分析协程的取消与异常之前,先要弄清楚父子协程的结构。
class JobDemo {
fun testJob() {
//父Job
var rootJob: Job? = null
runBlocking {
//启动子Job
var job1 = launch {
println("job1")
}
//启动子Job
var job2 = launch {
println("job2")
}
rootJob = coroutineContext[Job]
job1.join()
job2.join()
}
}
}
如上,通过runBlocking 启动一个协程,此时它作为父协程,在父协程里又依次启动了两个协程作为子协程。
launch()函数为CoroutineScope 的扩展函数,它的作用是启动一个协程:
#Builders.common.kt
fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
//构造新的上下文
val newContext = newCoroutineContext(context)
//协程
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
//开启
coroutine.start(start, coroutine, block)
//返回协程
return coroutine
}
以返回StandaloneCoroutine 为例,它继承自AbstractCoroutine,进而继承自JobSupport,而JobSupport 实现了Job接口,具体实现类即为JobSupport。
我们知道协程是比较抽象的事物,而Job 作为协程具象性的表达,表示协程的作业。
通过Job,我们可以控制、监控协程的一些状态,如:
//属性
job.isActive //协程是否活跃
job.isCancelled //协程是否被取消
job.isCompleted//协程是否执行完成
...
//函数
job.join()//等待协程完成
job.cancel()//取消协程
job.invokeOnCompletion()//注册协程完成回调
...
Job 的存储
Demo里通过launch()启动了两个子协程,暴露出来两个子Job,而它们的父Job 在哪呢?
从runBlocking()里寻找答案:
#Builers.kt
fun runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {
//...
//创建BlockingCoroutine,它也是个Job
val coroutine = BlockingCoroutine(newContext, currentThread, eventLoop)
coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
return coroutine.joinBlocking()
}
BlockingCoroutine 继承自AbstractCoroutine,AbstractCoroutine里有个成员变量:
#AbstractCoroutine.kt
//this 指代AbstractCoroutine 本身,也就是BlockingCoroutine
public final override val context: CoroutineContext = parentContext + this
不仅是BlockingCoroutine,StandaloneCoroutine 也继承自AbstractCoroutine,由此可见:
Job实例索引存储在对应的Context(上下文)里,通过context[Job]即可索引到具体的Job对象。
父子Job 关联
绑定关系初步建立
我们通常说的协程是结构化并发,它的状态比如异常可以在协程之间传递,怎么理解结构化这概念呢?重点在于理解父子协程、平级子协程之间是如何关联的。
还是上面的Demo,稍微改造:
fun testJob2() {
runBlocking {//父Job==rootJob
//启动子Job
var job1 = launch {
println("job1")
}
}
}
从job1的创建开始分析,先看AbstractCoroutine 的实现:
#AbstractCoroutine.kt
abstract class AbstractCoroutine(
parentContext: CoroutineContext,//父协程的上下文
initParentJob: Boolean,//是否需要关联父子Job,默认true
active: Boolean //默认true
) : JobSupport(active), Job, Continuation, CoroutineScope {
init {
//关联父子Job
//parentContext[Job] 即为从父Context里取出父Job
if (initParentJob) initParentJob(parentContext[Job])
}
}
#JobSupport.kt
protected fun initParentJob(parent: Job?) {
if (parent == null) {
//没有父Job,根Job 没有父Job
parentHandle = NonDisposableHandle
return
}
parent.start() // make sure the parent is started
//绑定父子Job ①
val handle = parent.attachChild(this)
//返回父Handle,指向链表 ②
parentHandle = handle
//...
}
分两个点 ①和 ②,先看①:
#JobSupport.kt
//ChildJob 为接口,接口里的函数是用来给父Job取消其子Job用的
//JobSupport 实现了ChildJob 接口
public final override fun attachChild(child: ChildJob): ChildHandle {
//ChildHandleNode(child) 构造ChildHandleNode 对象
return invokeOnCompletion(onCancelling = true, handler = ChildHandleNode(child).asHandler) as ChildHandle
}
#JobSupport.kt
public final override fun invokeOnCompletion(
onCancelling: Boolean,
invokeImmediately: Boolean,
handler: CompletionHandler
): DisposableHandle {
//创建
val node: JobNode = makeNode(handler, onCancelling)
loopOnState { state ->
when (state) {
//根据state,组合为一个ChildHandleNode 的链表
//比较繁琐,忽略
//返回链表头
}
}
}
最终的目的是返回ChildHandleNode,它可能是个链表。
再看②,将返回的结果记录在子Job的parentHandle 成员变量里。
小结一下:
- 父Job 构造ChildHandleNode 节点放入到链表里,每个节点存储的是子Job以及父Job 本身,而该链表可以与父Job里的state 互转。
- 子Job 的成员变量parentHandle 指向该链表。
由1.2 步骤可知,子Job 通过parentHandle 可以访问父Job,而父Job 通过state可以找出其下关联的子Job,如此父子Job就建立起了联系。
Job 链构建
上面分析了父子Job 之间是如何建立联系的,接下来重点分析子Job之间是如何关联的。
重点看看ChildHandleNode 的构造:
#JobSupport.kt
//主要有2个成员变量
//childJob: ChildJob 表示当前node指向的子Job
//parent: Job 表示当前node 指向的父Job
internal class ChildHandleNode(
@JvmField val childJob: ChildJob
) : JobCancellingNode(), ChildHandle {
override val parent: Job get() = job
//父Job 取消其所有子Job
override fun invoke(cause: Throwable?) = childJob.parentCancelled(job)
//子Job向上传递,取消父Job
override fun childCancelled(cause: Throwable): Boolean = job.childCancelled(cause)
}
可以看出,ChildHandleNode 里的invoke()、childCancelled()函数最终都依靠Job 实现其功能。
通过查找,很容易发现parentCancelled()/childCancelled()函数在JobSupport 均有实现。
ChildHandleNode 最终继承自LockFreeLinkedListNode,该类是一个线程安全的双向链表,双向链表我们很容易想到其实现的核心是依赖前驱后驱指针。
#LockFreeLinkedList.kt
public actual open class LockFreeLinkedListNode {
//后驱指针
private val _next = atomic(this) // Node | Removed | OpDescriptor
//前驱指针
private val _prev = atomic(this) // Node to the left (cannot be marked as removed)
private val _removedRef = atomic(null) // lazily cach
}
于是ChildHandleNode 链表如下图:
这样子Job 之间就通过前驱/后驱指针联系起来了。
再结合实际的Demo来阐述Job 链构造过程。
fun testJob2() {
runBlocking {//父Job==rootJob
//启动子Job
var job1 = launch {
println("job1")
}
//启动子Job
var job2 = launch {
println("job2")
}
cancel("")
}
}
第1步
runBlocking 创建一个协程,并构造Job,该Job为BlockingCoroutine,在创建Job的同时会尝试绑定父Job,而此时它作为根Job,没有父Job,因此parentHandle = NonDisposableHandle。
而这个时候,它还没创建子Job,因此state 里没有子Job。
第2步
创建第1个Job:Job1。
此时构造的Job为StandaloneCoroutine,在创建Job的同时会尝试绑定父Job,从父Context里取出父Job,即为BlockingCoroutine,找到后就开始进行关联绑定。
于是,现在的结构变为:
父Job 的state(指向链表头)此时就是个链表,该链表里的节点为ChildHandleNode,而ChildHandleNode 里存储了父Job与子Job。
第3步
创建第2个Job:Job2。
同样的,构造的Job 为StandaloneCoroutine,绑定父Job,最终的结构变为:
小结来说:
- 创建Job 时尝试关联其父Job。
- 若父Job 存在,则构造ChildHandleNode,该Node 存储了父Job以及子Job,并将ChildHandleNode 存储在父Job 的State里,同时子Job 的parentHandle 指向ChildHandleNode。
- 再次创建Job,继续尝试关联父Job,因为父Job 里已经关联了一个子Job,因此需要将新的子Job 挂到前一个子Job 后面,这样就形成了一个子Job链表。
简单Job 示意图:
如图,类似一个树结构。
当Job 链建立起来后,状态的传递就简单了。
- 父Job 通过链表可以找到每个子Job。
- 子Job 通过parentHandle 找到父Job。
- 子Job 之间通过链表索引。
由于篇幅原因,协程的取消与异常将在下篇分析,敬请关注。
本文基于Kotlin 1.5.3,文中完整Demo请点击
您若喜欢,请点赞、关注、收藏,您的鼓励是我前进的动力
持续更新中,和我一起步步为营系统、深入学习Android/Kotlin
1、Android各种Context的前世今生
2、Android DecorView 必知必会
3、Window/WindowManager 不可不知之事
4、View Measure/Layout/Draw 真明白了
5、Android事件分发全套服务
6、Android invalidate/postInvalidate/requestLayout 彻底厘清
7、Android Window 如何确定大小/onMeasure()多次执行原因
8、Android事件驱动Handler-Message-Looper解析
9、Android 键盘一招搞定
10、Android 各种坐标彻底明了
11、Android Activity/Window/View 的background
12、Android Activity创建到View的显示过
13、Android IPC 系列
14、Android 存储系列
15、Java 并发系列不再疑惑
16、Java 线程池系列
17、Android Jetpack 前置基础系列
18、Android Jetpack 易懂易学系列
19、Kotlin 轻松入门系列
20、Kotlin 协程系列全面解读
共有 0 条评论