在 Kotlin 協(xié)程當(dāng)中,我們通常把異常分為兩大類喇澡,一類是取消異常(CancellationException
)殊校,另一類是其他異常呕屎。之所以要這么分類秀睛,是因?yàn)樵?Kotlin 協(xié)程當(dāng)中蹂安,這兩種異常的處理方式是不一樣的田盈≡是疲或者說,在 Kotlin 協(xié)程所有的異常當(dāng)中畦韭,我們需要把CancellationException
單獨(dú)拎出來廊驼,特殊對(duì)待妒挎。
當(dāng)協(xié)程任務(wù)被取消的時(shí)候酝掩,協(xié)程內(nèi)部是會(huì)產(chǎn)生一個(gè) CancellationException
期虾。很多初學(xué)者都會(huì)遇到一個(gè)問題镶苞,那就是協(xié)程無法被取消茂蚓。帶著這個(gè)問題進(jìn)入下面的內(nèi)容晾浴。
一脊凰、協(xié)程的取消需要內(nèi)部配合
先看看下面的例子
fun main() {
runBlocking {
printMsg("start")
val job = launch(Dispatchers.IO) {
var i = 0
while (true) {
Thread.sleep(500L)
i++
printMsg("i = $i")
}
}
delay(2000L)
job.cancel() <------2秒后在協(xié)程作用域內(nèi)取消協(xié)程
job.join()
printMsg("end")
}
}
//日志
main @coroutine#1 start
DefaultDispatcher-worker-1 @coroutine#2 i = 1
DefaultDispatcher-worker-1 @coroutine#2 i = 2
DefaultDispatcher-worker-1 @coroutine#2 i = 3
//停不下來了
......
為什么2秒后協(xié)程沒有退出呢?這是因?yàn)閰f(xié)程是協(xié)作式的最岗,我們?cè)谧饔糜騼?nèi)調(diào)用了cancel
方法惶楼,協(xié)程需要自己檢查取消狀態(tài)歼捐,并在適當(dāng)?shù)臅r(shí)機(jī)主動(dòng)做出響應(yīng)豹储。取消狀態(tài)可以通過協(xié)程的 isActive
屬性進(jìn)行檢查剥扣。但是在我們的代碼中,由于是無限循環(huán)曙聂,協(xié)程沒有時(shí)機(jī)主動(dòng)檢查取消狀態(tài)断国,因此協(xié)程無法感知到取消請(qǐng)求并退出稳衬。
改造上面的代碼
fun main() {
runBlocking {
printMsg("start")
val job = launch(Dispatchers.IO) {
var i = 0
while (isActive) { <----------方式一:主動(dòng)提供檢查的時(shí)機(jī)
Thread.sleep(500L)
//delay(500L) <----------方式二:掛起函數(shù),在掛起點(diǎn)檢查自己的狀態(tài)
i++
printMsg("i = $i")
}
}
delay(2000L)
job.cancel()
job.join()
printMsg("end")
}
}
//日志
提供了二種方式:
方式一:通過
isActive
主動(dòng)發(fā)起狀態(tài)檢查弄砍。方式二:將
sleep
改為掛起函數(shù)delay
,因?yàn)閽炱鸷瘮?shù)在掛起或恢復(fù)的時(shí)候肯定會(huì)檢查協(xié)程的狀態(tài)(比如協(xié)程已經(jīng)被cancel
肯定不會(huì)再從掛起恢復(fù)了)莱坎。
綜上檐什,協(xié)程代碼如果無法被cancel
乃正,請(qǐng)檢查協(xié)程是否有檢查狀態(tài)的時(shí)機(jī)瓮具。
二名党、不要打破協(xié)程的父子結(jié)構(gòu)
看下面的例子
var startTime: Long = 0
fun main() {
runBlocking {
startTime = System.currentTimeMillis()
printMsg("start")
var childJob1: Job? = null
var childJob2: Job? = null
val parentJob = launch(Dispatchers.IO) {
childJob1 = launch { <------子協(xié)程使用父協(xié)程的上下文
printMsg("childJob1 start")
delay(600L) <------子協(xié)程掛起600毫秒后執(zhí)行完
printMsg("childJob1 end")
}
childJob2 = launch(Job()) { <------子協(xié)程使用自己的上下文
printMsg("childJob2 start")
delay(600L) <------子協(xié)程掛起600毫秒后執(zhí)行完
printMsg("childJob2 end")
}
}
delay(400L)
parentJob.cancel() <---------400毫秒后取消父協(xié)程
printMsg("childJob1.isActive=${childJob1?.isActive}") <-----程序執(zhí)行完時(shí)打印子協(xié)程1的狀態(tài)
printMsg("childJob2.isActive=${childJob2?.isActive}") <-----程序執(zhí)行完時(shí)打印子協(xié)程2的狀態(tài)
printMsg("end") <-----程序執(zhí)行完時(shí)
}
}
fun printMsg(msg: Any) {
println("打印內(nèi)容:$msg 消耗時(shí)間:${System.currentTimeMillis() - startTime} 線程信息:${Thread.currentThread().name} ")
}
//日志
打印內(nèi)容:start 消耗時(shí)間:0 線程信息:main @coroutine#1
打印內(nèi)容:childJob1 start 消耗時(shí)間:15 線程信息:DefaultDispatcher-worker-3 @coroutine#3
打印內(nèi)容:childJob2 start 消耗時(shí)間:22 線程信息:DefaultDispatcher-worker-2 @coroutine#4
打印內(nèi)容:childJob1.isActive=false 消耗時(shí)間:430 線程信息:main @coroutine#1
打印內(nèi)容:childJob2.isActive=true 消耗時(shí)間:430 線程信息:main @coroutine#1 <------程序執(zhí)行完時(shí),子協(xié)程2并沒有退出欧啤,isActive=true
打印內(nèi)容:end 消耗時(shí)間:430 線程信息:main @coroutine#1
Process finished with exit code 0
代碼并不難邢隧,注釋也很詳細(xì)府框∑染福可以看到子協(xié)程2使用自己的上下文后脫離了父協(xié)程的控制系宜,當(dāng)父協(xié)程被cancel
后盹牧,子協(xié)程2并沒有被cancel
口柳,isActive
狀態(tài)仍然是true
跃闹。
所以望艺,不要打破協(xié)程的父子結(jié)構(gòu)找默!
三惩激、不要用 try-catch 直接包裹 launch、async
看下面的例子
fun main() {
runBlocking {
printMsg("start")
try {
printMsg("try start")
launch {
printMsg("launch start")
delay(200L)
1 / 0 <------------200毫秒后創(chuàng)建一個(gè)異常
printMsg("launch end")
}
printMsg("try end")
} catch (exception: Exception) {
printMsg("catch $exception")
}
printMsg("end")
}
}
//日志
main @coroutine#1 start
main @coroutine#1 try start
main @coroutine#1 try end
main @coroutine#1 end
main @coroutine#2 launch start
Exception in thread "main" java.lang.ArithmeticException: / by zero <-----報(bào)錯(cuò)程序崩潰
雖然try-catch
包裹了協(xié)程的內(nèi)容魄咕,但是程序還是報(bào)錯(cuò)哮兰,這是因?yàn)樽訁f(xié)程與父協(xié)程是并發(fā)執(zhí)行的喝滞,它們之間是獨(dú)立的執(zhí)行流程右遭,所以上面代碼中父協(xié)程的 try-catch
無法捕獲子協(xié)程拋出的異常窘哈。
用try-catch
修改上面的代碼
fun main() {
runBlocking {
printMsg("start")
launch {
printMsg("launch start")
try {
printMsg("try start")
delay(200L)
1 / 0
printMsg("try end")
} catch (exception: Exception) {
printMsg("catch $exception")
}
printMsg("launch end")
}
printMsg("end")
}
}
//日志
main @coroutine#1 start
main @coroutine#1 end
main @coroutine#2 launch start
main @coroutine#2 try start
main @coroutine#2 catch java.lang.ArithmeticException: / by zero <------異常被成功捕獲
main @coroutine#2 launch end
Process finished with exit code 0
如果使用async
創(chuàng)建協(xié)程滚婉,try-catch
是應(yīng)該包裹async
內(nèi)的代碼塊還是應(yīng)該包裹deferred.await()
? 寫段代碼看看
fun main() {
runBlocking {
printMsg("start")
val deferred = async() {
printMsg("async start")
delay(200L)
1 / 0
printMsg("async end")
}
try {
deferred.await()
} catch (exception: Exception) {
printMsg("catch $exception")
}
printMsg("end")
}
}
//日志
main @coroutine#1 start
main @coroutine#2 async start
main @coroutine#1 catch java.lang.ArithmeticException: / by zero <------捕獲到了異常
main @coroutine#1 end
Exception in thread "main" java.lang.ArithmeticException: / by zero <-----報(bào)錯(cuò)程序崩潰
雖然捕獲到了異常远剩,但是程序還是報(bào)錯(cuò)了瓜晤,所以try-catch
一般還是包裹具體的代碼塊吧痢掠。
四、使用SupervisorJob
上面的一段代碼try-catch
住deferred.await()
仍然報(bào)錯(cuò)蛔钙,有沒有辦法補(bǔ)救這段代碼呢吁脱?答案是有兼贡,可以使用SupervisorJob()
遍希。代碼如下:
fun main() {
runBlocking {
printMsg("start")
val deferred = async(SupervisorJob()) { <-------變化在這里
printMsg("async start")
delay(200L)
1 / 0
printMsg("async end")
}
try {
deferred.await()
} catch (exception: Exception) {
printMsg("catch $exception")
}
printMsg("end")
}
}
//日志
main @coroutine#1 start
main @coroutine#2 async start
main @coroutine#1 catch java.lang.ArithmeticException: / by zero
main @coroutine#1 end
Process finished with exit code 0
為什么加了SupervisorJob()
就不報(bào)錯(cuò)了? 看下SupervisorJob()
的源碼:
@Suppress("FunctionName")
public fun SupervisorJob(parent: Job? = null) : CompletableJob = SupervisorJobImpl(parent)
public interface CompletableJob : Job {
public fun complete(): Boolean
public fun completeExceptionally(exception: Throwable): Boolean
}
SupervisorJob()
其實(shí)不是構(gòu)造函數(shù)胁黑,它只是一個(gè)普通的頂層函數(shù)漂洋。而這個(gè)方法返回的對(duì)象刽漂,是 Job
的子類爽冕。默認(rèn)的 Job
類型會(huì)將異常傳播給父協(xié)程颈畸,如果一個(gè)子協(xié)程拋出異常眯娱,它會(huì)取消父協(xié)程及其所有兄弟協(xié)程徙缴。
通過使用 SupervisorJob
疏叨,我們可以創(chuàng)建一個(gè)具有獨(dú)立異常處理行為的作業(yè)層級(jí)穿剖。這意味著即使子協(xié)程中發(fā)生異常秀又,父協(xié)程仍然可以繼續(xù)執(zhí)行而不會(huì)被取消贬芥,從而避免整個(gè)程序崩潰蘸劈。
SupervisorJob()
可以作為 CoroutineScope
的上下文威沫,但是它的監(jiān)管范圍并不是無限大的救巷,看下面的例子:
fun main() {
runBlocking {
val supervisorJob = SupervisorJob()
val scope = CoroutineScope(coroutineContext + supervisorJob) <-----作用域內(nèi)使用SupervisorJob()
val job = scope.launch { <----注意這里,作用域內(nèi)啟動(dòng)子協(xié)程
launch { <----注意這里,作用域內(nèi)啟動(dòng)孫協(xié)程
printMsg("job1 start")
delay(200L)
throw ArithmeticException("by zero")
}
launch {
printMsg("job2 start")
delay(300L)
printMsg("job2 end") <----關(guān)注這個(gè)日志
}
}
job.join()
scope.cancel()
}
}
//日志
main @coroutine#3 job1 start
main @coroutine#4 job2 start
Exception in thread "main @coroutine#4" java.lang.ArithmeticException: by zero
Process finished with exit code 0
上面的日志中并沒有輸出job2 end
浦译,說明上面job1
的異常影響了下面協(xié)程job2
的執(zhí)行,那如何修改呢叹俏?
fun main() {
runBlocking {
val supervisorJob = SupervisorJob()
val scope = CoroutineScope(coroutineContext + supervisorJob)
scope.apply { <----------變化在這里,launch改為apply
val job1 = launch {
printMsg("job1 start")
delay(200L)
throw ArithmeticException("by zero")
}
val job2 = launch {
printMsg("job2 start")
delay(300L)
printMsg("job2 end")
}
job1.join() <----------變化在這里
job2.join()
}
scope.cancel()
}
}
//日志
main @coroutine#2 job1 start
main @coroutine#3 job2 start
Exception in thread "main @coroutine#2" java.lang.ArithmeticException: by zero
main @coroutine#3 job2 end <--------成功輸出: job2 end
Process finished with exit code 0
可以看到當(dāng)將 SupervisorJob
作為 CoroutineScope
的上下文時(shí)屡谐,它的監(jiān)管范圍僅限于該作用域內(nèi)部啟動(dòng)的子協(xié)程愕掏。
SupervisorJob
的源碼中是因?yàn)橹貙懥?code>childCancelled方法并直接返回false
饵撑,保證異常不會(huì)向父協(xié)程和其他子協(xié)程傳遞:
private class SupervisorJobImpl(parent: Job?) : JobImpl(parent) {
override fun childCancelled(cause: Throwable): Boolean = false
}
事實(shí)上kotlin有提供給我們含SupervisorJob
上下文的協(xié)程作用域,它就是supervisorScope
语卤,源碼如下:
/**
* Creates a [CoroutineScope] with [SupervisorJob] and calls the specified suspend block with this scope.
* The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, but overrides
* context's [Job] with [SupervisorJob].
* This function returns as soon as the given block and all its child coroutines are completed.
*
* Unlike [coroutineScope], a failure of a child does not cause this scope to fail and does not affect its other children,
* so a custom policy for handling failures of its children can be implemented. See [SupervisorJob] for additional details.
* A failure of the scope itself (exception thrown in the [block] or external cancellation) fails the scope with all its children,
* but does not cancel parent job.
*
* The method may throw a [CancellationException] if the current job was cancelled externally,
* or rethrow an exception thrown by the given [block].
*/
public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return suspendCoroutineUninterceptedOrReturn { uCont ->
val coroutine = SupervisorCoroutine(uCont.context, uCont) <-------SupervisorCoroutine
coroutine.startUndispatchedOrReturn(coroutine, block)
}
}
private class SupervisorCoroutine<in T>(
context: CoroutineContext,
uCont: Continuation<T>
) : ScopeCoroutine<T>(context, uCont) {
override fun childCancelled(cause: Throwable): Boolean = false <-------同樣重寫了childCancelled方法返回false
}
我們使用supervisorScope
改造上面的代碼:
fun main() {
runBlocking {
supervisorScope {
val job1 = launch {
printMsg("job1 start")
delay(200L)
throw ArithmeticException("by zero")
}
val job2 = launch {
printMsg("job2 start")
delay(300L)
printMsg("job2 end")
}
job1.join()
job2.join()
}
}
}
//日志
main @coroutine#2 job1 start
main @coroutine#3 job2 start
Exception in thread "main @coroutine#2" java.lang.ArithmeticException: by zero
main @coroutine#3 job2 end <--------成功輸出: job2 end
Process finished with exit code 0
五蓖宦、CoroutineExceptionHandler
有時(shí)候由于協(xié)程嵌套的層級(jí)很深稠茂,并且也不需要每一個(gè)協(xié)程去處理異常睬关,這時(shí)候CoroutineExceptionHandler
就可以派上用場(chǎng)了,如下:
fun main() {
runBlocking {
val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
printMsg("CoroutineExceptionHandler $throwable")
}
val scope = CoroutineScope(coroutineExceptionHandler)
val job = scope.launch {
launch {
printMsg("job1 start")
delay(200L)
throw ArithmeticException("by zero")
}
launch {
printMsg("job2 start")
delay(300L)
printMsg("job2 end")
}
}
job.join()
scope.cancel()
}
}
//日志
DefaultDispatcher-worker-2 @coroutine#3 job1 start
DefaultDispatcher-worker-3 @coroutine#4 job2 start
DefaultDispatcher-worker-3 @coroutine#4 CoroutineExceptionHandler java.lang.ArithmeticException: by zero
Process finished with exit code 0
在CoroutineExceptionHandler
中成功輸出了異常的日志毡证。試試把CoroutineExceptionHandler
放在子協(xié)程報(bào)錯(cuò)的地方有什么樣的結(jié)果?
fun main() {
runBlocking {
val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
printMsg("CoroutineExceptionHandler $throwable")
}
val scope = CoroutineScope(coroutineContext) <--------變化在這里
val job = scope.launch {
launch(coroutineExceptionHandler) { <--------變化在這里
printMsg("job1 start")
delay(200L)
throw ArithmeticException("by zero")
}
launch {
printMsg("job2 start")
delay(300L)
printMsg("job2 end")
}
}
job.join()
scope.cancel()
}
}
//日志
main @coroutine#3 job1 start
main @coroutine#4 job2 start
Exception in thread "main" java.lang.ArithmeticException: by zero <-------程序報(bào)錯(cuò)
Process finished with exit code 1
程序報(bào)錯(cuò)电爹,且coroutineExceptionHandler
并沒有捕獲到異常,說明coroutineExceptionHandler
并沒有起到作用料睛,原因是CoroutineExceptionHandler
只在頂層的協(xié)程當(dāng)中才會(huì)起作用丐箩,當(dāng)子協(xié)程當(dāng)中出現(xiàn)異常以后,它們都會(huì)統(tǒng)一上報(bào)給頂層的父協(xié)程恤煞,然后由頂層的父協(xié)程去調(diào)用 CoroutineExceptionHandler
來處理異常屎勘。
看上面的日志都沒有輸出job2 end
,說明job1
的異常影響到了job2
的執(zhí)行居扒,那如果既想用coroutineExceptionHandler
兜底異常喜喂,又不想?yún)f(xié)程間因?yàn)楫惓铛绰;ハ嘤绊懺趺崔k呢这嚣? 我們可以試試這樣寫:
fun main() {
runBlocking {
val supervisorJob = SupervisorJob() <----------使用SupervisorJob()
val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
printMsg("CoroutineExceptionHandler $throwable")
}
val scope = CoroutineScope(coroutineExceptionHandler + supervisorJob) <-------加入到作用域的上下文
scope.apply {
val job1 = launch {
printMsg("job1 start")
delay(100L)
throw NullPointerException("parameters is null") <-----子協(xié)程的異常
}
val job2 = launch {
printMsg("job2 start")
delay(200L)
launch { <-----孫協(xié)程
try {
1 / 0 <-----孫協(xié)程的異常
} catch (exception: ArithmeticException) {
throw ArithmeticException("by zero") <------記得拋出來,不拋出來也沒有的
}
}
}
val job3 = launch {
printMsg("job3 start")
delay(300L)
printMsg("job3 end")
}
job1.join()
job2.join()
job3.join()
}
scope.cancel()
}
}
//日志
DefaultDispatcher-worker-1 @coroutine#2 job1 start
DefaultDispatcher-worker-2 @coroutine#3 job2 start
DefaultDispatcher-worker-3 @coroutine#4 job3 start
DefaultDispatcher-worker-2 @coroutine#2 CoroutineExceptionHandler java.lang.NullPointerException: parameters is null
DefaultDispatcher-worker-3 @coroutine#5 CoroutineExceptionHandler java.lang.ArithmeticException: by zero
DefaultDispatcher-worker-3 @coroutine#4 job3 end
Process finished with exit code 0