热点新闻
Kotlin 协程之取消与异常处理探索之旅(上)
2023-07-31 17:05  浏览:2199  搜索引擎搜索“手机低淘网”
温馨提示:信息一旦丢失不一定找得到,请务必收藏信息以备急用!本站所有信息均是注册会员发布如遇到侵权请联系文章中的联系方式或客服删除!
联系我时,请说明是在手机低淘网看到的信息,谢谢。
展会发布 展会网站大全 报名观展合作 软文发布

前言

协程系列文章:

  • 一个小故事讲明白进程、线程、Kotlin 协程到底啥关系?
  • 少年,你可知 Kotlin 协程最初的样子?
  • 讲真,Kotlin 协程的挂起/恢复没那么神秘(故事篇)
  • 讲真,Kotlin 协程的挂起/恢复没那么神秘(原理篇)
  • Kotlin 协程调度切换线程是时候解开真相了
  • Kotlin 协程之线程池探索之旅(与Java线程池PK)
  • Kotlin 协程之取消与异常处理探索之旅(上)
  • Kotlin 协程之取消与异常处理探索之旅(下)
  • 来,跟我一起撸Kotlin runBlocking/launch/join/async/delay 原理&使用

我们知道线程可以被终止,线程里可以抛出异常,类似的协程也会遇到此种情况。本篇将从线程的终止与异常处理分析开始,逐渐引入协程的取消与异常处理。
通过本篇文章,你将了解到:

  1. 线程的终止
  2. 线程的异常处理
  3. 协程的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<String>) { var threadDemo = ThreadDemo() threadDemo.testStop() }

结果如下:





image.png


可以看出,"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") } }

运行结果如下:





image.png

可以看出,线程启动后,中断线程,而最后线程依然正常运行到结束,说明此时线程并没有被中断。
本质原因:

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表示该线程被中断了,于是我们手动停止计数。
结果如下:





image.png


由此可见,线程被成功终止了。

综上所述,如何终止一个线程我们有了结论:





image.png

更加深入的分析原理以及两者的结合使用请移步:Java “优雅”地中断线程(实践篇)

2. 线程的异常处理

不论在Java 还是Kotlin里,异常都是可以通过try...catch 捕获。
典型如下:

fun testException() { try { 1/0 } catch (e : Exception) { println("e:$e") } }

结果:





image.png


成功捕获了异常。

改造一下Demo:

fun testException() { try { //开启线程 thread { 1/0 } } catch (e : Exception) { println("e:$e") } }

大家先猜测一下结果,能够捕获异常吗?
接着来看结果:





image.png


很遗憾,无法捕获。
根本原因:

异常的捕获是针对当前线程的堆栈。而上述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)方法。
结果如下:





image.png


说明成功捕获了异常。

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 <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T { //... //创建BlockingCoroutine,它也是个Job val coroutine = BlockingCoroutine<T>(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<in T>( parentContext: CoroutineContext,//父协程的上下文 initParentJob: Boolean,//是否需要关联父子Job,默认true active: Boolean //默认true ) : JobSupport(active), Job, Continuation<T>, 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 成员变量里。
小结一下:

  1. 父Job 构造ChildHandleNode 节点放入到链表里,每个节点存储的是子Job以及父Job 本身,而该链表可以与父Job里的state 互转。
  2. 子Job 的成员变量parentHandle 指向该链表。

由1.2 步骤可知,子Job 通过parentHandle 可以访问父Job,而父Job 通过state可以找出其下关联的子Job,如此父子Job就建立起了联系。





image.png

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<Any>(this) // Node | Removed | OpDescriptor //前驱指针 private val _prev = atomic(this) // Node to the left (cannot be marked as removed) private val _removedRef = atomic<Removed?>(null) // lazily cach }

于是ChildHandleNode 链表如下图:





image.png

这样子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。




image.png

第2步
创建第1个Job:Job1。
此时构造的Job为StandaloneCoroutine,在创建Job的同时会尝试绑定父Job,从父Context里取出父Job,即为BlockingCoroutine,找到后就开始进行关联绑定。
于是,现在的结构变为:




image.png

父Job 的state(指向链表头)此时就是个链表,该链表里的节点为ChildHandleNode,而ChildHandleNode 里存储了父Job与子Job。

第3步
创建第2个Job:Job2。
同样的,构造的Job 为StandaloneCoroutine,绑定父Job,最终的结构变为:




image.png

小结来说:

  1. 创建Job 时尝试关联其父Job。
  2. 若父Job 存在,则构造ChildHandleNode,该Node 存储了父Job以及子Job,并将ChildHandleNode 存储在父Job 的State里,同时子Job 的parentHandle 指向ChildHandleNode。
  3. 再次创建Job,继续尝试关联父Job,因为父Job 里已经关联了一个子Job,因此需要将新的子Job 挂到前一个子Job 后面,这样就形成了一个子Job链表。

简单Job 示意图:





image.png

如图,类似一个树结构。
当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 协程系列全面解读

发布人:073d****    IP:139.201.13.***     举报/删稿
展会推荐
让朕来说2句
评论
收藏
点赞
转发