1. 引言
僅知道協(xié)程中可以用CoroutineExceptionHandler來捕獲區(qū)里異常避免閃退,是遠(yuǎn)遠(yuǎn)不夠的虏辫,因為協(xié)程中的異常傳遞與處理部分认然,與協(xié)程結(jié)構(gòu)化并發(fā)部分息息相關(guān)妓布,一不小心堡掏,非常容易踩到坑上!
2. 異常干擾其它協(xié)程
在協(xié)程作用域的介紹使用當(dāng)中吕座,創(chuàng)建協(xié)程作用域的方式是:
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
為什么這個代碼要顯示構(gòu)造SupervisorJob()
虐译?這是個好問題,與本文內(nèi)容息息相關(guān)吴趴。
因為顯式構(gòu)造這個的功能漆诽,只有在多協(xié)程并產(chǎn)生協(xié)程結(jié)構(gòu)化的時候,才會表現(xiàn)出來锣枝。
先再多創(chuàng)建一個協(xié)程作用域厢拭,方便與前面的作用域?qū)ο笞鲗Ρ龋?/p>
private val jobScope = CoroutineScope(Job() + Dispatchers.Main)
然后,接下來撇叁,為方面對比實(shí)際效果供鸠,封裝下面的函數(shù):
private fun launchTenSecondsCoroutine(scope: CoroutineScope, extraMsg: String) {
scope.launch(Dispatchers.IO) {
val targetMilli = System.currentTimeMillis() + TEN_SECONDS
while (true) {
ensureActive()
if (System.currentTimeMillis() > targetMilli) {
break
}
myLog("launchLoopingCoroutine $extraMsg")
Thread.sleep(ONE_SECOND)
}
}
}
很簡單,就是用函數(shù)參數(shù)傳遞進(jìn)來的協(xié)程作用域啟動一個IO協(xié)程陨闹,這個協(xié)程會在10秒中之內(nèi)不斷循環(huán)楞捂,同時每次循環(huán)開啟前提供一個協(xié)程取消協(xié)作點(diǎn)薄坏,使得協(xié)程可以被取消。
再封裝一個啟動協(xié)程一個協(xié)程并在執(zhí)行的5秒后會拋出異常的函數(shù):
private fun launchCoroutineThrowException(scope: CoroutineScope, extraMsg: String) {
scope.launch(exceptionHandler + Dispatchers.IO) {
Thread.sleep(FIVE_SECONDS)
throw IllegalStateException("$extraMsg Throw exception!")
}
}
這里啟動協(xié)程前傳入了協(xié)程異常處理者對象:
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
myLog("CoroutineExceptionHandler:$throwable")
}
捕獲到異常時僅打印log信息寨闹。
現(xiàn)在胶坠,到了真正的實(shí)踐代碼環(huán)節(jié),通過前面jobScope
對象來啟動3個協(xié)程繁堡,即兩個僅執(zhí)行10秒的協(xié)程和一個執(zhí)行5秒后會拋出異常且?guī)в袇f(xié)程異常處理者的協(xié)程:
private fun launchWithJobClicked() {
myLog("launchWithJobClicked")
launchTenSecondsCoroutine(jobScope, "launchWithJobClicked A")
launchTenSecondsCoroutine(jobScope, "launchWithJobClicked B")
launchCoroutineThrowException(jobScope, "launchWithJobClicked")
}
小小賣個關(guān)子沈善,這里不妨先想一下:應(yīng)用會因為異常的拋出而閃退嗎?如果不會椭蹄,那么協(xié)程A和協(xié)程B矮瘟,實(shí)際的執(zhí)行時間大概有多長?
最終的結(jié)果會是:
應(yīng)用不會因為協(xié)程的拋出而閃退塑娇,在5秒時有一個異常觸發(fā)CoroutineExceptionHandler的處理,但協(xié)程A和協(xié)程B劫侧,實(shí)際的執(zhí)行時間只有5秒左右(根據(jù)log的輸出情況判斷),永遠(yuǎn)達(dá)不到10秒烧栋。
是不是很詭異?明明協(xié)程A跟B的執(zhí)行邏輯审姓,如果協(xié)程沒有被取消的話,明明會執(zhí)行10秒的魔吐!為什么這里的log在5秒后就不再輸出了呢?
等等酬姆,注意前面這句話嗜桌,“如果協(xié)程沒有被取消的話”,從最終的執(zhí)行結(jié)果反過來想辞色,會不會是因為協(xié)程被取消了呢骨宠?但是這時候又沒有點(diǎn)擊取消按鈕啊,為什么協(xié)程被取消了呢相满?
這便是一個協(xié)程異常處理者的一個大坑层亿,當(dāng)一個協(xié)程中遇到的異常用CoroutineExceptionHandler處理以后,默認(rèn)情況下立美,當(dāng)前協(xié)程會將遇到的異常繼續(xù)向父協(xié)程中傳遞并取消父協(xié)程匿又,而父協(xié)程的取消必然會取消其所有子協(xié)程。
回到實(shí)踐代碼上悯辙,由于launchCoroutineThrowException
中啟動的協(xié)程和協(xié)程A/B三者之間會為兄弟協(xié)程琳省,三者有個共同的父Job迎吵,即為jobScope
對象在構(gòu)造時所創(chuàng)建Job對象。
當(dāng)launchCoroutineThrowException
的協(xié)程中拋出了異常且被CoroutineExceptionHandler處理后针贬,會進(jìn)一步地取消父Job击费,而父Job的取消,從而使得其另外的兩個子協(xié)程A和B被取消桦他。
所以蔫巩,這便解析了,為什么協(xié)程A和協(xié)程B在執(zhí)行過程中只執(zhí)行5秒左右快压。
這里還需要注意圆仔,在launchWithJobClicked
在頁面第一次被調(diào)用的時候,啟動的三個協(xié)程均得到執(zhí)行蔫劣,只不過是5秒后會結(jié)束坪郭,而在這個函數(shù)第二次及以后執(zhí)行中,所啟動的協(xié)程函數(shù)體中的內(nèi)容將不再獲得執(zhí)行脉幢,不妨再想想歪沃,為什么?此非本文重點(diǎn)嫌松,但這個對協(xié)程結(jié)構(gòu)化并發(fā)的理解其實(shí)也很重要沪曙。
3. 異常不干擾其它協(xié)程
因為某一個協(xié)程出現(xiàn)異常導(dǎo)致其父協(xié)程以及其他的兄弟協(xié)程的取消,這種場景肯定是有的萎羔,比如同時發(fā)出一系列請求贾陷,有一個請求出現(xiàn)異常而失敗時將導(dǎo)致最終請求結(jié)果為失敗髓废,這時候出現(xiàn)異常后及時取消其他協(xié)程,是合理的砸喻。
但是割岛,如果希望協(xié)程間互相獨(dú)立呢癣漆?即某一個協(xié)程因異常而結(jié)束惠爽,并不希望其影響其他協(xié)程,因為同一個協(xié)程作用域啟動的協(xié)程租副,可能是互相影響結(jié)果用僧,也可能是互相獨(dú)立互不影響的部分责循。
這時候攀操,便是SupervisorJob()
發(fā)揮作用的時候了意蛀。
前面啟動的協(xié)程是通過jobScope:
private val jobScope = CoroutineScope(Job() + Dispatchers.Main)
現(xiàn)在將換用scope對象:
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
啟動協(xié)程的內(nèi)容和第2節(jié)中相同,只不過傳入的協(xié)程作用域?qū)ο髲?code>jobScope換成了scope
:
private fun launchWithSupervisorJobClicked() {
launchTenSecondsCoroutine(scope, "launchWithSupervisorJobClicked A")
launchTenSecondsCoroutine(scope, "launchWithSupervisorJobClicked B")
launchCoroutineThrowException(scope, "launchWithSupervisorJobClicked")
}
這時候秀姐,log的輸出結(jié)果听哭,是5秒時拋出一個異常被CoroutineExceptionHandler處理,而A/B兩個協(xié)程的log輸出在10秒內(nèi)始終輸出舷蟀,而且多次執(zhí)行launchWithSupervisorJobClicked
野宜,里面啟動的協(xié)程函數(shù)體部分始終會獲得執(zhí)行河胎。
這里便是SupervisorJob()
和Job()
兩種寫法導(dǎo)致的差異了虎敦,在某個協(xié)程拋出異常并被CoroutineExceptionHandler處理時,當(dāng)前協(xié)程會不會取消其父協(xié)程喷户。
嗯摩骨?前面好像說這個協(xié)程也會取消其兄弟協(xié)程恼五?為什么這里不寫后半句了灾馒?首先睬罗,再回第2節(jié)看看表述容达,并沒有協(xié)程拋出異常后會取消兄弟協(xié)程一說花盐,在第2節(jié)中的情況菇爪,兄弟協(xié)程的取消不是因為當(dāng)前協(xié)程遇到異常凳宙,而是因為父協(xié)程的取消必然會取消子協(xié)程氏涩,這部分是結(jié)構(gòu)化并發(fā)方面對于協(xié)程取消的設(shè)計奖亚,當(dāng)前協(xié)程不會取消父協(xié)程了析砸,所以兄弟協(xié)程自然就不會被取消作郭。。
而SupervisorJob()
和Job()
的區(qū)別蜘醋,僅僅只是,產(chǎn)生的Job對象會不會因子協(xié)程的異常而被取消胎食。
4. 創(chuàng)建協(xié)程作用域的考慮
通過第2節(jié)和第3節(jié)的對比厕怜,可以看出粥航,同一個協(xié)程作用域所啟動的協(xié)程之間是否會因異常而互相干擾递雀,關(guān)鍵是創(chuàng)建協(xié)程作用域之時所創(chuàng)建的Job元素對象是哪一種缀程。
如果希望其中一個協(xié)程產(chǎn)生異常而自動取消其他協(xié)程并后續(xù)不再能啟動協(xié)程,那么應(yīng)該用Job()
秕衙;如果不希望某個協(xié)程產(chǎn)生異常時會影響到其他協(xié)程并后續(xù)可以繼續(xù)啟動協(xié)程,那么應(yīng)該用SupervisorJob()
搞糕。
在Android平臺頁面的設(shè)計上窍仰,更多情況下應(yīng)該是SupervisorJob()
,同時Android中提供的lifeCycleScope
和viewModelScope
兩種實(shí)現(xiàn)中所用的均是SupervisorJob()
晶伦。
這里必要再提及一句,當(dāng)用CoroutineScope(context: CoroutineContext)
這種方式創(chuàng)建協(xié)程作用域時泌参,如果不顯式提供Job元素的對象沽一,那么最終協(xié)程作用域中的Job元素對象類型將會是Job()
。結(jié)合前面第二節(jié)的實(shí)踐代碼結(jié)果攘残,請謹(jǐn)慎在創(chuàng)建協(xié)程作用域時不提供Job元素的方式歼郭,比如謹(jǐn)慎使用CoroutineScope(Dispatchers.Main)
這種寫法創(chuàng)建作用域?qū)ο蟛≡堑?節(jié)的是所實(shí)踐結(jié)果是業(yè)務(wù)需要的泰涂。
5. 結(jié)構(gòu)化并發(fā)中關(guān)于異常處理
在初識結(jié)構(gòu)化一文當(dāng)中,在“進(jìn)階版等待多協(xié)程完成”一節(jié)中寄疏,介紹的掛起函數(shù)是supervisorScope
是牢,注意看這個函數(shù)的命名,是否與前面介紹的兩種Job之間的差異單詞陕截,是否很類似驳棱?是的,并不是不知道有coroutineScope
這個农曲,也知道不少技術(shù)文章中均有使用這個coroutineScope
這個函數(shù)作為結(jié)構(gòu)化并發(fā)介紹的社搅,前文中是故意地使用supervisorScope
,避免后續(xù)觸及使用到CoroutineExceptionHandler
時踩到隱藏的坑。
其實(shí)罚渐,在初始結(jié)構(gòu)化一文中的實(shí)踐代碼却汉,用到的supervisorScope
的地方,換用coroutineScope
也會是同樣的結(jié)果,而這兩者之間的區(qū)別,只有在啟動的協(xié)程中使用CoroutineExceptionHandler
且遇上異常拋出時才會體現(xiàn)出來。
先說明一下,coroutineScope
也是個掛起函數(shù)桨吊,而前面創(chuàng)建協(xié)程作用域用的方法CoroutineScope(CoroutineContext)
是個普通頂層函數(shù)(并不是構(gòu)造函數(shù))留美,注意首字母的大小寫以及后面的參數(shù)區(qū)別以及是否掛起函數(shù)的區(qū)別奕枝。
說得有點(diǎn)多了忘晤,還是上代碼吧闰蛔。
private fun suspendWithJobClicked() {
myLog("suspendWithJobClicked")
scope.launch(exceptionHandler) {
myLog("suspendWithJobClicked parent coroutine")
val ret = coroutineScope {
launchTenSecondsCoroutine(this, "suspendWithJobClicked A")
launchTenSecondsCoroutine(this, "suspendWithJobClicked B")
launchCoroutineThrowException(this, "suspendWithJobClicked")
"coroutineScope final line"
}
myLog("suspendWithJobClicked parent coroutine final line: $ret")
}
}
這里用掛起函數(shù)coroutineScope
掛起當(dāng)前協(xié)程裁着,并且用產(chǎn)生的子協(xié)程作用域啟動兩個執(zhí)行10秒的協(xié)程和一個5秒后拋出異常的協(xié)程诸蚕,并且將在coroutineScope
的lambda表達(dá)式最后一個表達(dá)式中返回字符串"coroutineScope final line"漠魏,在coroutineScope
后輸出log壤巷,log中帶有coroutineScope
的返回值有巧。
問:這里的執(zhí)行結(jié)果能打印出來"suspendWithJobClicked parent coroutine final line: xxx"這一行l(wèi)og嗎?如果能,log輸出的時間點(diǎn)是在協(xié)程啟動后的不久后命锄,還是5秒或10秒左右還是其他情況苟翻?最終的這一行l(wèi)og的拼接后的完整字符串內(nèi)容會是什么?
答:這里的執(zhí)行結(jié)果打印不出來"suspendWithJobClicked parent coroutine final line: xxx"一行l(wèi)og汗洒。
結(jié)合第2第3節(jié)的內(nèi)容,這里結(jié)果不對啊,這里外邊啟動協(xié)程時用的scope創(chuàng)建是用的已經(jīng)是SupervisorJob()
了赫模,為什么當(dāng)中啟動的三個協(xié)程還會被提前取消蒸矛?
注意啊乡话,這里的啟動協(xié)程的封裝函數(shù)中傳入的this對象颅停,是coroutineScope
中創(chuàng)建的子協(xié)程作用域而不是scope
對象本身喊熟,所以思考的應(yīng)該是coroutineScope
中創(chuàng)建的協(xié)程作用域是Job
類型還是SupervisorJob
類型钥勋,很遺憾,從結(jié)果上看骑冗,是Job
類型(源碼上也是)谢翎。
所以,這時候啟動的3個協(xié)程的中結(jié)果沐旨,等同于第2節(jié)森逮,即一個協(xié)程拋出異常,最終會導(dǎo)致其父協(xié)程以及兄弟協(xié)程的取消磁携,從而使得最后一行l(wèi)og輸出的代碼不被執(zhí)行褒侧。
那如果希望3個協(xié)程的執(zhí)行互不干擾,想要第3節(jié)中類似的執(zhí)行結(jié)果谊迄,該如何闷供?很簡單,換用supervisorScope
即可:
private fun suspendWithSupervisorJobClicked() {
myLog("suspendWithSupervisorJobClicked")
scope.launch {
myLog("suspendWithSupervisorJobClicked parent coroutine")
val ret = supervisorScope {
launchTenSecondsCoroutine(this, "suspendWithSupervisorJobClicked A")
launchTenSecondsCoroutine(this, "suspendWithSupervisorJobClicked B")
launchCoroutineThrowException(this, "suspendWithSupervisorJobClicked")
"supervisorScope final line"
}
myLog("suspendWithSupervisorJobClicked parent coroutine final line: $ret")
}
}
問:這里的執(zhí)行結(jié)果能打印出來"suspendWithSupervisorJobClickedparent coroutine final line: xxx"這一行l(wèi)og嗎统诺?如果能歪脏,log輸出的時間點(diǎn)是在協(xié)程啟動后的不久后,還是5秒或10秒左右還是其他情況粮呢?最終的這一行l(wèi)og的拼接后的完整字符串內(nèi)容會是什么婿失?
答:能打印出來這行l(wèi)og,輸出的時間點(diǎn)在10秒后啄寡,最后一行l(wèi)og的完整字符串會是
"suspendWithSupervisorJobClickedparent coroutine final line: supervisorScope final line"豪硅。
這里,不妨簡單對比下coroutineScope
和supervisorScope
兩者挺物,因為這兩者才是真正的同一層面的可對比函數(shù):
- 如果啟動的子協(xié)程中均沒有拋出異常懒浮,兩者在功能上沒有區(qū)別;
- 如果啟動的子協(xié)程中任意一個某一時刻拋出了異常且用CoroutineExceptionHandler進(jìn)行處理识藤,那么前者的其他子協(xié)程會再異常拋出后被取消砚著,后者的其他子協(xié)程不受影響次伶;
附:目前見不少地方會用coroutineScope
和CoroutineScope(context: CoroutineContext)
這兩者作比較,事實(shí)上這兩者除了函數(shù)名起得特別像以后稽穆,函數(shù)的使用范圍冠王、設(shè)計功能都是截然不同的,并沒有什么可比較性秧骑。
6. 樣例工程代碼
代碼樣例Demo,見Github:https://github.com/TeaCChen/CoroutineStudy
本文示例代碼扣囊,如覺奇怪或啰嗦乎折,其實(shí)為SupervisorActivity.kt
中的代碼摘取主要部分說明,在demo代碼當(dāng)中侵歇,為提升細(xì)節(jié)內(nèi)容骂澄,有更加多的封裝和輸出內(nèi)容。
本文的頁面截圖示例如下:
7. 補(bǔ)充說明
對于協(xié)程的異常處理惕虑,實(shí)際代碼開發(fā)當(dāng)中是要結(jié)合協(xié)程作用域坟冲、并發(fā)處理、協(xié)程取消協(xié)程等等內(nèi)容來進(jìn)一步按照需求來進(jìn)行設(shè)計溃蔫,協(xié)程當(dāng)中各種概念都不是孤島健提,往往講解一部分內(nèi)容的時候會設(shè)計到另一部分的補(bǔ)充設(shè)計,學(xué)習(xí)或使用協(xié)程的過程中請盡量保持好奇和探索伟叛。
基礎(chǔ)篇的內(nèi)容到此為止私痹,如果能看到這里,對于系列內(nèi)容中统刮,協(xié)程啟動紊遵、協(xié)程中切換線程、協(xié)程的取消侥蒙、協(xié)程作用域暗膜、掛起函數(shù)、結(jié)構(gòu)化并發(fā)鞭衩、協(xié)程異常等內(nèi)容有了個大概的實(shí)踐或認(rèn)識学搜,對于launch
、withContext
论衍、Dispatchers.IO
恒水、Dispatchers.Main
、ensureActive
饲齐、CoroutineScope
钉凌、join
、async
捂人、await
御雕、supervisorScope
矢沿、CoroutineExceptionHandler
等關(guān)鍵內(nèi)容有個大概的認(rèn)識。
基礎(chǔ)篇整體內(nèi)容酸纲,服務(wù)于對協(xié)程拿起就用的實(shí)用主義捣鲸,同時將協(xié)程的各項設(shè)計(協(xié)程作用域、掛起函數(shù)闽坡、取消栽惶、異常、結(jié)構(gòu)化并發(fā))從實(shí)踐代碼中逐步帶出疾嗅,為的是對協(xié)程使用有個較為系統(tǒng)的認(rèn)識外厂。
在協(xié)程的使用上,自然可以有“一看就會”這種更加通俗易懂的介紹內(nèi)容代承,而且也不用分九個部分來逐一實(shí)踐汁蝶,但是個人認(rèn)為,在協(xié)程的使用上论悴,欲速則不達(dá)掖棉。這一系列內(nèi)容,個人相信是一學(xué)就會的膀估,對于里面各種講解的各種情況幔亥,也給出了相應(yīng)的項目版完整代碼,有實(shí)踐察纯、有對比也有講解紫谷,剩下的,就是思考了捐寥。
我思故我在笤昨。
慢者,為快握恳。
一學(xué)就會的協(xié)程使用——基礎(chǔ)篇(一)協(xié)程啟動
一學(xué)就會的協(xié)程使用——基礎(chǔ)篇(二)線程切換
一學(xué)就會的協(xié)程使用——基礎(chǔ)篇(三)初遇協(xié)程取消
一學(xué)就會的協(xié)程使用——基礎(chǔ)篇(四)協(xié)程作用域
一學(xué)就會的協(xié)程使用——基礎(chǔ)篇(五)再遇協(xié)程取消
一學(xué)就會的協(xié)程使用——基礎(chǔ)篇(六)初識掛起
一學(xué)就會的協(xié)程使用——基礎(chǔ)篇(七)初識結(jié)構(gòu)化
一學(xué)就會的協(xié)程使用——基礎(chǔ)篇(八)初識協(xié)程異常
一學(xué)就會的協(xié)程使用——基礎(chǔ)篇(九)異常與supervisor(本文)