kotlin 協(xié)程:一文搞定異常傳遞與處理

前言

本文全面解釋協(xié)程的異常傳遞機(jī)制以及處理方式鹰祸,需要一定的協(xié)程基礎(chǔ)。擺脫只會(huì)使用 try catch 的尷尬取逾,以更優(yōu)雅和更靈活的方式處理異常逆趣。

異常傳遞

Job

對(duì)于普通 Job 來(lái)說(shuō)荆几,異常的傳遞是雙向的吓妆,即異常會(huì)向子協(xié)程和父協(xié)程傳播,流程為:

  • 當(dāng)前協(xié)程出現(xiàn)異常
  • cancel 子協(xié)程
  • 等待子協(xié)程 cancel 完成吨铸,cancel 自己
  • 傳遞給父協(xié)程并循環(huán)前面步驟

可見行拢,異常會(huì)自下而上地傳播,任一協(xié)程發(fā)生異常會(huì)影響到整個(gè)協(xié)程樹诞吱。

image.png

如下代碼中舟奠,child 1出現(xiàn)異常后,child 3房维、child 2沼瘫、child 0 依次會(huì)被取消。

CoroutineScope(Job()).launch { // child 0
    launch {// child 1
        launch { // child 3
            delay(1000)
            println("child 3")
        }
        throw Exception("test")
    }
    launch { // child 2
        delay(1000)
        println("child 2")
    }
    delay(1000)
    println("child 0")
}.join()

SupervisorJob

如果不希望協(xié)程內(nèi)的異常向上傳播或影響同級(jí)協(xié)程握巢≡稳担可以使用 SupervisorJob 松却。

SuperVisorJob 可以使子協(xié)程的異常向上傳播到 SupervisorJob 層時(shí)不被處理暴浦,即異常向上傳播在 SupervisorJob 處終止。

image.png

如上圖所示晓锻,SupervisorJobScope 的任一直接子協(xié)程發(fā)生異常都不會(huì)影響其他直接子協(xié)程歌焦,更不會(huì)向上傳播影響父協(xié)程。

注意砚哆,父協(xié)程異常依然會(huì)導(dǎo)致子協(xié)程取消独撇,這點(diǎn)和 Job 一致。

CoroutineScope(Job()).launch { // child 0
    val supervisorJob = SupervisorJob()
    launch(supervisorJob) {// child 1
        launch { // child 3
            delay(1000)
            println("child 3")
        }
        throw Exception("test")
    }
    launch(supervisorJob) { // child 2
        delay(1000)
        println("child 2")
    }
    delay(1000)
    println("child 0")
}.join()

還是上面的例子躁锁,使用 SupervisorJob 作為 child 1 和 child 2 的父 Job纷铣,這樣 child 2 中的異常只能向下影響 child 3 ,避免了異常影響到 child 0 和 child 2战转。

也可以使用 supervisorScope 搜立,它是一個(gè)自帶了 SupervisorJob 的協(xié)程作用域,效果是一樣的:

CoroutineScope(Job()).launch { // child 0
    supervisorScope {
        launch {// child 1
            launch { // child 3
                delay(1000)
                println("child 3")
            }
            throw Exception("test")
        }
        launch { // child 2
            delay(1000)
            println("child 2")
        }
    }
    delay(1000)
    println("child 0")
}.join()

不同的是槐秧,supervisorScope 是掛起方法啄踊,會(huì)掛起協(xié)程直至所有子協(xié)程執(zhí)行完成。

注意刁标,易錯(cuò) ??

下面代碼中颠通,誰(shuí)是 Child1 的父協(xié)程?

val scope = CoroutineScope(Job())
scope.launch(SupervisorJob()) {
   launch {
        // Child 1
    }
    launch {
        // Child 2
    }
}

Child 的父協(xié)程類型是 Job 膀懈,并不是 SupervisorJob 顿锰!

scope.launch 會(huì)創(chuàng)建一個(gè)新 Job,該 Job 的父 Job 是 launch 時(shí)指定的,所以 SupervisorJob 是新 Job 的父 Job 硼控,而 scope 創(chuàng)建時(shí)傳入的 Job 被 SupervisorJob 覆蓋乘客,因而協(xié)程關(guān)系為:

SupervisorJob -〉Job -〉(child1、child2)淀歇,因而上述代碼中易核,SupervisorJob 沒(méi)有起到該有的作用。

image.png

鑒于上述原因浪默,可以:

val scope = CoroutineScope(SupervisorJob())
scope.launch {
    // Child 1
}
scope.launch {
    // Child 2
}

也可以使用 supervisorScope 達(dá)到預(yù)期效果:

supervisorScope {
    launch { // Child 1
        throw IllegalArgumentException()
    }
    launch { // Child 2
        delay(1000)
        println("child 2 run over") //可順利執(zhí)行完成
    }
    delay(1000)
    println("Job: ${coroutineContext[Job]?.javaClass}")
}

打印結(jié)果:

Exception in thread "Test worker @coroutine#2" java.lang.IllegalArgumentException
Job: class kotlinx.coroutines.SupervisorCoroutine
child 2 run over

不管是什么 Job牡直,異常傳遞如果不做處理,最終會(huì)到達(dá)線程的 UncaughtExceptionHandler纳决,如果是 JVM 則輸出在控制臺(tái)碰逸,如果是 Android 沒(méi)設(shè)置 UncaughtExceptionHandler 則會(huì)出現(xiàn) app 崩潰(如果是主線程則一定崩潰)。

異常處理

對(duì)于不同協(xié)程構(gòu)造器阔加,異常的處理方式不同饵史。分別介紹 launchasync 情況下的異常處理。

Launch

  • try catch

    launch 方式啟動(dòng)的協(xié)程胜榔,異常會(huì)在發(fā)生時(shí)立刻拋出胳喷,使用 try catch 就可以將協(xié)程中的異常捕獲。如:

    scope.launch {
        try {
            codeThatCanThrowExceptions()
        } catch(e: Exception) {
            // Handle exception
        }
    }
    
    

    try catch 整個(gè)協(xié)程也是可以的:

    try {
        coroutineScope {
            codeThatCanThrowExceptions()
        }
    } catch (t: Throwable) {
        // Handle exception
    }
    
    

    注意夭织,這樣是不可以的 ? :

    try {
        CoroutineScope().launch {
            codeThatCanThrowExceptions()
        }
    } catch (t: Throwable) {
        // Handle exception
    }
    
    

    因?yàn)?launch 不是掛起函數(shù)吭露。

  • CoroutineExceptionHandler

    除了使用 try catch ,更推薦使用 CoroutineExceptionHandler 對(duì)異常進(jìn)行統(tǒng)一處理尊惰。需要注意的是異常會(huì)層層代理到根協(xié)程讲竿,所以 CoroutineExceptionHandler 只能在根協(xié)程中才能生效。

    比如這樣 ? :

    CoroutineScope(Job()).launch(CoroutineExceptionHandler { coroutineContext, throwable ->
        println("catch ex successfully")
    }) {
        throw RuntimeException()
    }
    
    

    或者這樣 ? 弄屡,根協(xié)程會(huì)繼承該 CoroutineExceptionHandler

    CoroutineScope(CoroutineExceptionHandler { coroutineContext, throwable ->
        println("catch ex successfully")
    }).launch() {
        throw RuntimeException()
    }
    
    

    而這樣是不對(duì)的 ? :

    coroutineScope {
        launch(CoroutineExceptionHandler { coroutineContext, throwable ->
            println("catch ex failed")
        }) {
            throw RuntimeException()
        }
    }
    
    

    子協(xié)程異常會(huì)代理給父協(xié)程题禀,一直向上傳遞直到根協(xié)程,如果找到 CoroutineExceptionHandler 則處理膀捷,否則走 UncaughtExceptionHandler 迈嘹。可見担孔,其他子協(xié)程中的 CoroutineExceptionHandler 不會(huì)起到作用江锨。

    猜猜被誰(shuí)捕獲?:

    CoroutineScope(Job() + CoroutineExceptionHandler { coroutineContext, throwable ->
        println("catch ex in scope")
    }).launch(CoroutineExceptionHandler { coroutineContext, throwable -> 
        println("catch ex in top Coroutine")
    }) {
        throw RuntimeException()
    }
    
    

    結(jié)論:scope 中的 CoroutineExceptionHandler 會(huì)覆蓋糕篇。

    這種將異常代理給父協(xié)程的行為可以被 SupervisorJob 改變啄育,將異常交給子協(xié)程自己處理:

    supervisorScope {
        launch(CoroutineExceptionHandler { coroutineContext, throwable ->
            println("catch ex successfully")
        }) {
            throw RuntimeException()
        }
    }
    
    

    異常傳遞到 SupervisorJob 處停止,交由 SupervisorJob 的直接子協(xié)程處理拌消,這時(shí) CoroutineExceptionHandler 是生效的挑豌。

    根協(xié)程:由 scope 直接調(diào)用 launchasync 開啟的協(xié)程安券。

Async

  • try catch

    當(dāng) async 開啟的協(xié)程為根協(xié)程,或 SupervisorJob 的直接子協(xié)程時(shí)氓英,異常在調(diào)用 await 時(shí)拋出侯勉,使用 try catch 可以捕獲異常:

    /**
     * async 開啟的協(xié)程為根協(xié)程
     */
    fun main() = runBlocking {
        val deferred = GlobalScope.async {
            throw Exception()
        }
        try {
            deferred.await() //拋出異常
        } catch (t: Throwable) {
            println("捕獲異常:$t")
        }
    }
    
    
    /**
     * async 開啟的協(xié)程為 SupervisorJob 的直接子協(xié)程
     */
    fun main() = runBlocking {
        supervisorScope {
            val deferred = async {
                throw Exception()
            }
            try {
                deferred.await() //拋出異常
            } catch (t: Throwable) {
                println("捕獲異常:$t")
            }
        }
    }
    
    
    /**
     * async 開啟的協(xié)程為 SupervisorJob 的直接子協(xié)程
     */
    CoroutineScope(Job()).launch {
        val deferred = async(SupervisorJob()) {
            throw Exception()
        }
        try {
            deferred.await() //拋出異常
        } catch (t: Throwable) {
            println("捕獲異常:$t")
        }
    }
    
    
  • CoroutineExceptionHandler

    當(dāng):

    async 開啟的協(xié)程為根協(xié)程 或 supervisorScope 的直接子協(xié)程”

    的條件不成立時(shí),異常會(huì)在發(fā)生時(shí)立刻拋出并傳播铝阐,對(duì) await 進(jìn)行 try catch 就不起作用了:

    fun main(): Unit = runBlocking {
        supervisorScope {
            launch {
                val deferred = async { throw Exception() } //拋出異常
                try {
                    deferred.await()
                } catch (e: Exception) {
                    println("catch ex")
                }
                delay(1000)
                println("done")
            }
        }
    }
    
    

    控制臺(tái)中雖然打印了 “catch ex”址貌,但未能打印 “done”,這表明異常傳遞到了父協(xié)程徘键,父協(xié)程被取消练对。至于為什么還能打印 "catch ex",是因?yàn)?deferred.await() 會(huì)拋出異常吹害,async 中的異趁荆總會(huì)在 await 時(shí)拋出。如果在 await 前加個(gè) delay它呀,那么就看不到 "catch ex" 了螺男,因?yàn)?await 被取消了。

    可以使用 CoroutineExceptionHandler 進(jìn)行處理纵穿。CoroutineExceptionHandler 只在根協(xié)程和 SupervisorJob 的直接子協(xié)程有效下隧。因此需要在 launch 開啟的父協(xié)程進(jìn)行處理:

    /**
     * async 開啟的協(xié)程非 supervisorScope 的直接子協(xié)程,異常會(huì)直接拋出政恍,
     * try catch await 無(wú)效汪拥,但可由 CoroutineExceptionHandler 處理
     */
    fun main(): Unit = runBlocking {
        supervisorScope {
            launch(CoroutineExceptionHandler { coroutineContext, throwable ->
                println("CoroutineExceptionHandler 捕獲異常:$throwable")
            }) {
                val deferred = async { throw Exception() } //拋出異常
                deferred.await()
            }
        }
    }
    
    
    /**
     * async 開啟的協(xié)程非根協(xié)程,異常會(huì)直接拋出篙耗,
     * try catch await 無(wú)效,但可由 CoroutineExceptionHandler 處理
     */
    fun main(): Unit = runBlocking {
        CoroutineScope(Job()).launch(CoroutineExceptionHandler { coroutineContext, throwable ->
            println("CoroutineExceptionHandler 捕獲異常:$throwable")
        }) {
            val deferred = async { throw Exception() } //拋出異常
            deferred.await()
        }.join()
    }
    
    

總的來(lái)說(shuō)宪赶,不管是 launch 還是 async宗弯,使用 CoroutineExceptionHandler 的規(guī)則都是一致的,也更不易出錯(cuò)搂妻,推薦使用蒙保。

Cancellation 和 異常

CancellationException 總會(huì)被 CoroutineExceptionHandler 忽略,但能被 try catch 捕獲欲主,ok邓厕,又多了個(gè)使用 CoroutineExceptionHandler 的理由。

fun main() = runBlocking {
    CoroutineScope(Job()).launch(CoroutineExceptionHandler { coroutineContext, throwable ->
        println("CoroutineExceptionHandler 捕獲異常:$throwable")
    }) {
        println("運(yùn)行了")
        cancel()
        try {
            delay(1000)
        } catch (throwable: Throwable) {
            println("try catch 捕獲異常:$throwable")
            throw throwable
        }
    }.join()
}

運(yùn)行結(jié)果:

運(yùn)行了
try catch 捕獲異常:kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#2":StandaloneCoroutine{Cancelling}@54cda7ca

異常聚合

如果一個(gè)協(xié)程拋出不止一個(gè)異常扁瓢,那么則以第一個(gè)拋出的異常為主详恼,其他異常作為 suppressed 異常依附在主異常中。

源碼

關(guān)于異常的傳播控制邏輯可參見 JobSupport.kt 類的 notifyCancellingchildCancelled 方法引几,SupervisorJob 就是復(fù)寫了 childCancelled 方法才阻止了異常的向上傳播昧互。

結(jié)構(gòu)化并發(fā)

它是一種編程范式,旨在通過(guò)結(jié)構(gòu)化的方式使并發(fā)編程 更清晰明確、更高質(zhì)量敞掘、更易維護(hù)叽掘。

其核心有幾點(diǎn):

  • 通過(guò)把多線程任務(wù)進(jìn)行結(jié)構(gòu)化的包裝,使其具有明確的開始和結(jié)束點(diǎn)玖雁,并確保其孵化出的所有任務(wù)在退出前全部完成更扁。
  • 這種包裝允許結(jié)構(gòu)中線程發(fā)生的異常能夠傳播至結(jié)構(gòu)頂端的作用域,并且能夠被該語(yǔ)言原生異常機(jī)制捕獲赫冬。

kotlin 協(xié)程設(shè)計(jì)中的協(xié)程關(guān)系疯潭、執(zhí)行順序、異常傳播/處理 都符合結(jié)構(gòu)化并發(fā)面殖。結(jié)構(gòu)化并發(fā)明確了并發(fā)任務(wù)什么時(shí)候開始竖哩,什么時(shí)候結(jié)束,異常如何傳播脊僚,通過(guò)控制頂層結(jié)構(gòu)具柄就可實(shí)現(xiàn)整個(gè)并發(fā)結(jié)構(gòu)的取消相叁、異常處理,使復(fù)雜的并發(fā)問(wèn)題簡(jiǎn)單辽幌、清晰增淹、可控∥谄螅可以說(shuō)虑润,結(jié)構(gòu)化并發(fā)大大降低了并發(fā)編程的難度。

總結(jié)

  • 協(xié)程中未捕獲的異臣咏停總會(huì)向下取消子協(xié)程拳喻,向上傳遞異常,體現(xiàn)了結(jié)構(gòu)化并發(fā)的特點(diǎn)猪腕。
  • SupervisorJob 可以阻止異常繼續(xù)向上傳播冗澈,并將異常交給子協(xié)程處理。
  • launch 啟動(dòng)的協(xié)程在異常發(fā)生時(shí)總是立刻拋出陋葡,可以由 try catch 捕獲亚亲,也可以使用 CoroutineExceptionHandler 處理,注意 handler 使用的位置腐缤。
  • async 啟動(dòng)的協(xié)程在其為根協(xié)程或 supervisorScope 的直接子協(xié)程時(shí)捌归,異常會(huì)在 async 內(nèi)部捕獲,當(dāng) deferred 對(duì)象調(diào)用 await 時(shí)拋出岭粤,否則也會(huì)在異常發(fā)生時(shí)立刻拋出并傳播惜索。前者可以使用 try catch 捕獲 await 調(diào)用,當(dāng)然 CoroutineExceptionHandler 也可以绍在,而后者不能通過(guò) try catch 整個(gè) async 代碼塊或 await 調(diào)用捕獲異常门扇。
  • 結(jié)構(gòu)化并發(fā)大大降低了并發(fā)編程的難度雹有,kotlin 協(xié)程設(shè)計(jì)也遵循該編程范式。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末臼寄,一起剝皮案震驚了整個(gè)濱河市霸奕,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌吉拳,老刑警劉巖质帅,帶你破解...
    沈念sama閱讀 212,718評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異留攒,居然都是意外死亡煤惩,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門炼邀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)魄揉,“玉大人,你說(shuō)我怎么就攤上這事拭宁÷逋耍” “怎么了?”我有些...
    開封第一講書人閱讀 158,207評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵杰标,是天一觀的道長(zhǎng)兵怯。 經(jīng)常有香客問(wèn)我,道長(zhǎng)腔剂,這世上最難降的妖魔是什么媒区? 我笑而不...
    開封第一講書人閱讀 56,755評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮掸犬,結(jié)果婚禮上袜漩,老公的妹妹穿的比我還像新娘。我一直安慰自己登渣,他們只是感情好噪服,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,862評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著胜茧,像睡著了一般。 火紅的嫁衣襯著肌膚如雪仇味。 梳的紋絲不亂的頭發(fā)上呻顽,一...
    開封第一講書人閱讀 50,050評(píng)論 1 291
  • 那天,我揣著相機(jī)與錄音丹墨,去河邊找鬼廊遍。 笑死,一個(gè)胖子當(dāng)著我的面吹牛贩挣,可吹牛的內(nèi)容都是我干的喉前。 我是一名探鬼主播,決...
    沈念sama閱讀 39,136評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼舌剂!你這毒婦竟也來(lái)了请垛?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,882評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤见咒,失蹤者是張志新(化名)和其女友劉穎偿衰,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體改览,經(jīng)...
    沈念sama閱讀 44,330評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡下翎,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,651評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了宝当。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片视事。...
    茶點(diǎn)故事閱讀 38,789評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖庆揩,靈堂內(nèi)的尸體忽然破棺而出俐东,到底是詐尸還是另有隱情,我是刑警寧澤盾鳞,帶...
    沈念sama閱讀 34,477評(píng)論 4 333
  • 正文 年R本政府宣布犬性,位于F島的核電站,受9級(jí)特大地震影響腾仅,放射性物質(zhì)發(fā)生泄漏乒裆。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,135評(píng)論 3 317
  • 文/蒙蒙 一推励、第九天 我趴在偏房一處隱蔽的房頂上張望鹤耍。 院中可真熱鬧,春花似錦验辞、人聲如沸稿黄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)杆怕。三九已至,卻和暖如春壳贪,著一層夾襖步出監(jiān)牢的瞬間陵珍,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工违施, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留互纯,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,598評(píng)論 2 362
  • 正文 我出身青樓磕蒲,卻偏偏與公主長(zhǎng)得像留潦,于是被迫代替她去往敵國(guó)和親只盹。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,697評(píng)論 2 351

推薦閱讀更多精彩內(nèi)容