前言
協程系列文章:
我們知道線程可以被終止,線程里可以拋出異常,類似的協程也會遇到此種情況。本篇將從線程的終止與異常處理分析開始,逐漸引入協程的取消與異常處理。
通過本篇文章,你將了解到:
- 線程的終止
- 線程的異常處理
- 協程的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()
}
結果如下:
可以看出,"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 <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 成員變量里。
小結一下:
- 父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<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 之間就通過前驅/后驅指針聯系起來了。
再結合實際的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 協程系列全面解讀