Kotlin 協(xié)程之取消與異常處理探索之旅(上)

前言

協(xié)程系列文章:

我們知道線程可以被終止涨醋,線程里可以拋出異常瓜饥,類似的協(xié)程也會遇到此種情況。本篇將從線程的終止與異常處理分析開始浴骂,逐漸引入協(xié)程的取消與異常處理乓土。
通過本篇文章,你將了解到:

  1. 線程的終止
  2. 線程的異常處理
  3. 協(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()
}

結果如下:


image.png

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

運行結果如下:


image.png

可以看出喳挑,線程啟動后彬伦,中斷線程,而最后線程依然正常運行到結束伊诵,說明此時線程并沒有被中斷单绑。
本質原因:

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表示該線程被中斷了浙炼,于是我們手動停止計數份氧。
結果如下:


image.png

由此可見唯袄,線程被成功終止了弯屈。

綜上所述,如何終止一個線程我們有了結論:


image.png

更加深入的分析原理以及兩者的結合使用請移步:Java “優(yōu)雅”地中斷線程(實踐篇)

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(主)線程里進行捕獲窥妇,而異常時發(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)方法。
結果如下:


image.png

說明成功捕獲了異常辣辫。

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 成員變量里内地。
小結一下:

  1. 父Job 構造ChildHandleNode 節(jié)點放入到鏈表里,每個節(jié)點存儲的是子Job以及父Job 本身赋除,而該鏈表可以與父Job里的state 互轉阱缓。
  2. 子Job 的成員變量parentHandle 指向該鏈表。

由1.2 步驟可知举农,子Job 通過parentHandle 可以訪問父Job荆针,而父Job 通過state可以找出其下關聯(lián)的子Job,如此父子Job就建立起了聯(lián)系颁糟。


image.png

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 鏈表如下圖:


image.png

這樣子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。

image.png

第2步
創(chuàng)建第1個Job:Job1偷卧。
此時構造的Job為StandaloneCoroutine豺瘤,在創(chuàng)建Job的同時會嘗試綁定父Job,從父Context里取出父Job听诸,即為BlockingCoroutine坐求,找到后就開始進行關聯(lián)綁定。
于是晌梨,現在的結構變?yōu)椋?br>

image.png

父Job 的state(指向鏈表頭)此時就是個鏈表桥嗤,該鏈表里的節(jié)點為ChildHandleNode,而ChildHandleNode 里存儲了父Job與子Job仔蝌。

第3步
創(chuàng)建第2個Job:Job2砸逊。
同樣的,構造的Job 為StandaloneCoroutine掌逛,綁定父Job师逸,最終的結構變?yōu)椋?br>

image.png

小結來說:

  1. 創(chuàng)建Job 時嘗試關聯(lián)其父Job。
  2. 若父Job 存在豆混,則構造ChildHandleNode篓像,該Node 存儲了父Job以及子Job,并將ChildHandleNode 存儲在父Job 的State里皿伺,同時子Job 的parentHandle 指向ChildHandleNode员辩。
  3. 再次創(chuàng)建Job,繼續(xù)嘗試關聯(lián)父Job鸵鸥,因為父Job 里已經關聯(lián)了一個子Job奠滑,因此需要將新的子Job 掛到前一個子Job 后面丹皱,這樣就形成了一個子Job鏈表。

簡單Job 示意圖:


image.png

如圖宋税,類似一個樹結構摊崭。
當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é)程系列全面解讀

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末计螺,一起剝皮案震驚了整個濱河市夯尽,隨后出現的幾起案子,更是在濱河造成了極大的恐慌登馒,老刑警劉巖匙握,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異谊娇,居然都是意外死亡肺孤,警方通過查閱死者的電腦和手機罗晕,發(fā)現死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門济欢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人小渊,你說我怎么就攤上這事法褥。” “怎么了酬屉?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵半等,是天一觀的道長。 經常有香客問我呐萨,道長杀饵,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任谬擦,我火速辦了婚禮切距,結果婚禮上,老公的妹妹穿的比我還像新娘惨远。我一直安慰自己谜悟,他們只是感情好,可當我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布北秽。 她就那樣靜靜地躺著葡幸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪贺氓。 梳的紋絲不亂的頭發(fā)上蔚叨,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天,我揣著相機與錄音辙培,去河邊找鬼缅叠。 笑死,一個胖子當著我的面吹牛虏冻,可吹牛的內容都是我干的肤粱。 我是一名探鬼主播,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼厨相,長吁一口氣:“原來是場噩夢啊……” “哼领曼!你這毒婦竟也來了鸥鹉?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤庶骄,失蹤者是張志新(化名)和其女友劉穎毁渗,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體单刁,經...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡灸异,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了羔飞。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片肺樟。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖逻淌,靈堂內的尸體忽然破棺而出么伯,到底是詐尸還是另有隱情,我是刑警寧澤卡儒,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布田柔,位于F島的核電站,受9級特大地震影響骨望,放射性物質發(fā)生泄漏硬爆。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一擎鸠、第九天 我趴在偏房一處隱蔽的房頂上張望缀磕。 院中可真熱鬧,春花似錦糠亩、人聲如沸虐骑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽廷没。三九已至,卻和暖如春垂寥,著一層夾襖步出監(jiān)牢的瞬間颠黎,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工滞项, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留狭归,地道東北人。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓文判,卻偏偏與公主長得像过椎,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子戏仓,可洞房花燭夜當晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內容