前言
協(xié)程系列文章:
- 一個小故事講明白進程踏施、線程幌羞、Kotlin 協(xié)程到底啥關系斋陪?
- 少年瘤运,你可知 Kotlin 協(xié)程最初的樣子?
- 講真罩旋,Kotlin 協(xié)程的掛起/恢復沒那么神秘(故事篇)
- 講真啊央,Kotlin 協(xié)程的掛起/恢復沒那么神秘(原理篇)
- Kotlin 協(xié)程調度切換線程是時候解開真相了
- Kotlin 協(xié)程之線程池探索之旅(與Java線程池PK)
- Kotlin 協(xié)程之取消與異常處理探索之旅(上)
- Kotlin 協(xié)程之取消與異常處理探索之旅(下)
- 來,跟我一起擼Kotlin runBlocking/launch/join/async/delay 原理&使用
我們知道線程可以被終止涨醋,線程里可以拋出異常瓜饥,類似的協(xié)程也會遇到此種情況。本篇將從線程的終止與異常處理分析開始浴骂,逐漸引入協(xié)程的取消與異常處理乓土。
通過本篇文章,你將了解到:
- 線程的終止
- 線程的異常處理
- 協(xié)程的Job 結構
1. 線程的終止
如何終止一個線程
阻塞狀態(tài)下終止
先看個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()
}
結果如下:
可以看出溯警,"thread end" 沒有打印出來趣苏,說明線程被成功中斷了。
上述Demo里線程能夠被中斷的本質是:
Thread.sleep(xx)方法會檢測中斷狀態(tài)梯轻,若是發(fā)現發(fā)生了中斷食磕,則拋出異常。
非阻塞狀態(tài)下終止
改造一下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() 方法僅僅只是喚醒線程與設置中斷標記位。
此種場景下如何終止一個線程呢曹宴?我們繼續(xù)改造一下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 “優(yōu)雅”地中斷線程(實踐篇)
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(主)線程里進行捕獲窥妇,而異常時發(fā)生在子線程里。
你可能會說娩践,簡單我直接在子線程里進行捕獲即可活翩。
fun testException() {
thread {
try {
1/0
} catch (e : Exception) {
println("e:$e")
}
}
}
這么做沒毛病,很合理也很剛翻伺。
考慮另一種場景:若是主線程想要獲取子線程異常的原因材泄,進而做不同的處理。
這時候就引入了:UncaughtExceptionHandler吨岭。
繼續(xù)改造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")
}
}
其實就是注冊了個回調拉宗,當線程發(fā)生異常時會調用uncaughtException(xx)方法。
結果如下:
說明成功捕獲了異常辣辫。
3. 協(xié)程的Job 結構
Job 基礎
Job 的創(chuàng)建
在分析協(xié)程的取消與異常之前旦事,先要弄清楚父子協(xié)程的結構。
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 啟動一個協(xié)程族檬,此時它作為父協(xié)程,在父協(xié)程里又依次啟動了兩個協(xié)程作為子協(xié)程化戳。
launch()函數為CoroutineScope 的擴展函數单料,它的作用是啟動一個協(xié)程:
#Builders.common.kt
fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
//構造新的上下文
val newContext = newCoroutineContext(context)
//協(xié)程
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
//開啟
coroutine.start(start, coroutine, block)
//返回協(xié)程
return coroutine
}
以返回StandaloneCoroutine 為例,它繼承自AbstractCoroutine点楼,進而繼承自JobSupport扫尖,而JobSupport 實現了Job接口,具體實現類即為JobSupport掠廓。
我們知道協(xié)程是比較抽象的事物换怖,而Job 作為協(xié)程具象性的表達,表示協(xié)程的作業(yè)蟀瞧。
通過Job沉颂,我們可以控制、監(jiān)控協(xié)程的一些狀態(tài)悦污,如:
//屬性
job.isActive //協(xié)程是否活躍
job.isCancelled //協(xié)程是否被取消
job.isCompleted//協(xié)程是否執(zhí)行完成
...
//函數
job.join()//等待協(xié)程完成
job.cancel()//取消協(xié)程
job.invokeOnCompletion()//注冊協(xié)程完成回調
...
Job 的存儲
Demo里通過launch()啟動了兩個子協(xié)程铸屉,暴露出來兩個子Job,而它們的父Job 在哪呢切端?
從runBlocking()里尋找答案:
#Builers.kt
fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {
//...
//創(chuàng)建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 關聯(lián)
綁定關系初步建立
我們通常說的協(xié)程是結構化并發(fā)躬厌,它的狀態(tài)比如異常可以在協(xié)程之間傳遞竞帽,怎么理解結構化這概念呢烤咧?重點在于理解父子協(xié)程、平級子協(xié)程之間是如何關聯(lián)的抢呆。
還是上面的Demo煮嫌,稍微改造:
fun testJob2() {
runBlocking {//父Job==rootJob
//啟動子Job
var job1 = launch {
println("job1")
}
}
}
從job1的創(chuàng)建開始分析,先看AbstractCoroutine 的實現:
#AbstractCoroutine.kt
abstract class AbstractCoroutine<in T>(
parentContext: CoroutineContext,//父協(xié)程的上下文
initParentJob: Boolean,//是否需要關聯(lián)父子Job抱虐,默認true
active: Boolean //默認true
) : JobSupport(active), Job, Continuation<T>, CoroutineScope {
init {
//關聯(lián)父子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 {
//創(chuàng)建
val node: JobNode = makeNode(handler, onCancelling)
loopOnState { state ->
when (state) {
//根據state,組合為一個ChildHandleNode 的鏈表
//比較繁瑣谣沸,忽略
//返回鏈表頭
}
}
}
最終的目的是返回ChildHandleNode刷钢,它可能是個鏈表。
再看②乳附,將返回的結果記錄在子Job的parentHandle 成員變量里内地。
小結一下:
- 父Job 構造ChildHandleNode 節(jié)點放入到鏈表里,每個節(jié)點存儲的是子Job以及父Job 本身赋除,而該鏈表可以與父Job里的state 互轉阱缓。
- 子Job 的成員變量parentHandle 指向該鏈表。
由1.2 步驟可知举农,子Job 通過parentHandle 可以訪問父Job荆针,而父Job 通過state可以找出其下關聯(lián)的子Job,如此父子Job就建立起了聯(lián)系颁糟。
Job 鏈構建
上面分析了父子Job 之間是如何建立聯(lián)系的航背,接下來重點分析子Job之間是如何關聯(lián)的。
重點看看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 實現其功能键畴。
通過查找最盅,很容易發(fā)現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 鏈表如下圖:
這樣子Job 之間就通過前驅/后驅指針聯(lián)系起來了惹想。
再結合實際的Demo來闡述Job 鏈構造過程问词。
fun testJob2() {
runBlocking {//父Job==rootJob
//啟動子Job
var job1 = launch {
println("job1")
}
//啟動子Job
var job2 = launch {
println("job2")
}
cancel("")
}
}
第1步
runBlocking 創(chuàng)建一個協(xié)程,并構造Job嘀粱,該Job為BlockingCoroutine激挪,在創(chuàng)建Job的同時會嘗試綁定父Job,而此時它作為根Job锋叨,沒有父Job垄分,因此parentHandle = NonDisposableHandle。
而這個時候娃磺,它還沒創(chuàng)建子Job薄湿,因此state 里沒有子Job。
第2步
創(chuàng)建第1個Job:Job1偷卧。
此時構造的Job為StandaloneCoroutine豺瘤,在創(chuàng)建Job的同時會嘗試綁定父Job,從父Context里取出父Job听诸,即為BlockingCoroutine坐求,找到后就開始進行關聯(lián)綁定。
于是晌梨,現在的結構變?yōu)椋?br>
父Job 的state(指向鏈表頭)此時就是個鏈表桥嗤,該鏈表里的節(jié)點為ChildHandleNode,而ChildHandleNode 里存儲了父Job與子Job仔蝌。
第3步
創(chuàng)建第2個Job:Job2砸逊。
同樣的,構造的Job 為StandaloneCoroutine掌逛,綁定父Job师逸,最終的結構變?yōu)椋?br>
小結來說:
- 創(chuàng)建Job 時嘗試關聯(lián)其父Job。
- 若父Job 存在豆混,則構造ChildHandleNode篓像,該Node 存儲了父Job以及子Job,并將ChildHandleNode 存儲在父Job 的State里皿伺,同時子Job 的parentHandle 指向ChildHandleNode员辩。
- 再次創(chuàng)建Job,繼續(xù)嘗試關聯(lián)父Job鸵鸥,因為父Job 里已經關聯(lián)了一個子Job奠滑,因此需要將新的子Job 掛到前一個子Job 后面丹皱,這樣就形成了一個子Job鏈表。
簡單Job 示意圖:
如圖宋税,類似一個樹結構摊崭。
當Job 鏈建立起來后,狀態(tài)的傳遞就簡單了杰赛。
- 父Job 通過鏈表可以找到每個子Job呢簸。
- 子Job 通過parentHandle 找到父Job。
- 子Job 之間通過鏈表索引乏屯。
由于篇幅原因根时,協(xié)程的取消與異常將在下篇分析,敬請關注辰晕。
本文基于Kotlin 1.5.3蛤迎,文中完整Demo請點擊
您若喜歡,請點贊含友、關注忘苛、收藏,您的鼓勵是我前進的動力
持續(xù)更新中唱较,和我一起步步為營系統(tǒng)扎唾、深入學習Android/Kotlin
1、Android各種Context的前世今生
2南缓、Android DecorView 必知必會
3胸遇、Window/WindowManager 不可不知之事
4、View Measure/Layout/Draw 真明白了
5汉形、Android事件分發(fā)全套服務
6纸镊、Android invalidate/postInvalidate/requestLayout 徹底厘清
7、Android Window 如何確定大小/onMeasure()多次執(zhí)行原因
8概疆、Android事件驅動Handler-Message-Looper解析
9逗威、Android 鍵盤一招搞定
10、Android 各種坐標徹底明了
11岔冀、Android Activity/Window/View 的background
12凯旭、Android Activity創(chuàng)建到View的顯示過
13、Android IPC 系列
14使套、Android 存儲系列
15罐呼、Java 并發(fā)系列不再疑惑
16、Java 線程池系列
17侦高、Android Jetpack 前置基礎系列
18嫉柴、Android Jetpack 易懂易學系列
19、Kotlin 輕松入門系列
20奉呛、Kotlin 協(xié)程系列全面解讀