协程中的取消和异常 (异常处理详解)

某个协程运行出现异常怎么办?

当某个协程运行出现异常的时候,那么会有以下几个操作:

  • 取消自己的子级
  • 取消自己
  • 将异常传播给父级
    最新异常会到达CoroutineScope 的根本,并且CoroutineScope 启动的所有协程都会被取消

当协程出现异常只取消自己,不影响其他协程

当协程运行出现异常的时候,我们希望不影响其他协程,只取消自己即可,我们可以使用SupervisorJob 和supervisorScope

  • 例子1使用普通的Job
import kotlinx.coroutines.*

suspend fun main() {
// Scope 控制我的应用中某一层级的协程
    val scopeParent = CoroutineScope(Job())
    val scopeChild = CoroutineScope(Job())
    val scopeSChild = CoroutineScope(SupervisorJob())
    scopeParent.launch {
        //child1
        scopeChild.launch {
            1 / 0
        }
        //child2
        scopeChild.launch {
            delay(1000)
            println("你好")
        }
    }

    Thread.sleep(2000)
}

Exception in thread "DefaultDispatcher-worker-2" java.lang.ArithmeticException: / by zero
    at CoroutineKt$main$2$1.invokeSuspend(coroutine.kt:17)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
    at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:738)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)

使用普通的Job的时候,child1出现异常的时候child2也被取消,所以child2没有执行

  • 例子2使用SupervisorJob
import kotlinx.coroutines.*

suspend fun main() {
// Scope 控制我的应用中某一层级的协程
    val scopeParent = CoroutineScope(Job())
    val scopeChild = CoroutineScope(Job())
    val scopeSChild = CoroutineScope(SupervisorJob())
    scopeParent.launch {
        //child1
        scopeSChild.launch {
            1 / 0
        }
        //child2
        scopeSChild.launch {
            delay(1000)
            println("你好")
        }
    }

    Thread.sleep(2000)
}
Exception in thread "DefaultDispatcher-worker-1" java.lang.ArithmeticException: / by zero
    at CoroutineKt$main$2$1.invokeSuspend(coroutine.kt:11)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
    at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:738)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
你好

使用普通的SupervisorJob的时候,child1出现异常的时候child2继续执行

  • 例子3使用supervisorScope
import kotlinx.coroutines.*

suspend fun main() {
// Scope 控制我的应用中某一层级的协程
    val scopeParent = CoroutineScope(Job())
    val scopeChild = CoroutineScope(Job())
    val scopeSChild = CoroutineScope(SupervisorJob())
    scopeParent.launch {
        //child1
        supervisorScope {
            launch {
                1 / 0
            }
        }
        //child2
        supervisorScope {
            launch {
                delay(1000)
                println("你好")
            }
        }

    }

    Thread.sleep(2000)
}

Exception in thread "DefaultDispatcher-worker-2" java.lang.ArithmeticException: / by zero
    at CoroutineKt$main$2$1$1.invokeSuspend(coroutine.kt:12)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
    at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:738)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
你好

使用supervisorScope 也一样,child1出现异常,不会影响到child2

  • 例子3使用try catch
import kotlinx.coroutines.*
import java.lang.Exception

suspend fun main() {
// Scope 控制我的应用中某一层级的协程
    val scopeParent = CoroutineScope(Job())
    val scopeChild = CoroutineScope(Job())
    val scopeSChild = CoroutineScope(SupervisorJob())
    scopeParent.launch {
        //child1
        scopeChild.launch {
            try {
                1 / 0
            } catch (e: Exception) {
                println(e)
            }
        }
        //child2
        scopeChild.launch {
            delay(1000)
            println("你好")
        }

    }

    Thread.sleep(2000)
}
java.lang.ArithmeticException: / by zero
你好

使用try catch把child1的异常捕捉之后,也不会影响到child2

处理异常

我们使用launch启动的协程,我们使用try catch可以捕捉异常


scope.launch {
    try {
        codeThatCanThrowExceptions()
    } catch(e: Exception) {
        // 处理异常
    }
}

如果我们使用了async,用于返回结果的协程,异常会在我们调用.await()方法的时候抛出


supervisorScope {
    val deferred = async {
        codeThatCanThrowExceptions()
    }
 
   try {
       deferred.await()
   } catch(e: Exception) {
       // 处理 async 中抛出的异常
   }
}

所以我们在使用async的时候,不要将try catch放在async中,而是放在.await()中。

CoroutineExceptionHandler

我们知道CoroutineExceptionHandler是CoroutineContext 上下文的一个可选的元素,当我们没用使用try catch捕捉异常的时候,我们可以使用CoroutineExceptionHandler来捕捉异常。


val handler = CoroutineExceptionHandler {
   context, exception -> println("Caught $exception")
}

使用CoroutineExceptionHandler捕捉异常的前提是通过launch启动的协程
例如1:


import kotlinx.coroutines.*
import java.lang.Exception

suspend fun main() {
// Scope 控制我的应用中某一层级的协程
    val scopeParent = CoroutineScope(Job())
    val scopeChild = CoroutineScope(Job())
    val scopeSChild = CoroutineScope(SupervisorJob())

    val handler = CoroutineExceptionHandler { context, exception ->
        println("Caught $exception")
    }
    scopeParent.launch(handler) {
        1 / 0
        //child2
    }

    Thread.sleep(2000)


}

Caught java.lang.ArithmeticException: / by zero

例如2:

import kotlinx.coroutines.*

suspend fun main() {
// Scope 控制我的应用中某一层级的协程
    val scopeParent = CoroutineScope(Job())
    val scopeChild = CoroutineScope(Job())
    val scopeSChild = CoroutineScope(SupervisorJob())

    val handler = CoroutineExceptionHandler { context, exception ->
        println("Caught $exception")
    }
    scopeParent.launch {
        launch(handler) {
            1 / 0
        }
    }

    Thread.sleep(2000)


}

Exception in thread "DefaultDispatcher-worker-2" java.lang.ArithmeticException: / by zero
    at CoroutineKt$main$2$1.invokeSuspend(coroutine.kt:15)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
    at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:738)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)

以上协程的异常为什么没用被捕捉呢?因为我们在协程的内部又启动了一个子协程,当子协程报错的时候,直接抛给了父级,父级不知道handler的存在,所以就没用捕捉到。我们可以改成如下:

import kotlinx.coroutines.*

suspend fun main() {
// Scope 控制我的应用中某一层级的协程
    val scopeParent = CoroutineScope(Job())
    val scopeChild = CoroutineScope(Job())
    val scopeSChild = CoroutineScope(SupervisorJob())

    val handler = CoroutineExceptionHandler { context, exception ->
        println("Caught $exception")
    }
    scopeParent.launch(handler) {
        launch {
            1 / 0
        }
    }

    Thread.sleep(2000)


}
Caught java.lang.ArithmeticException: / by zero

我们让父级接受handler 就能捕捉到了异常

小结

  • 协程内部出现异常的时候会直接取消自己和孩子并上报给父亲,最后导致整个作用域取消
  • 让协程出现异常不影响其他协程,我们可以使用SupervisorJob和supervisorScope
  • 处理协程的异常我们可以使用try catch,当启动协程的方法是aysnc的时候,协程的异常会在调用awaite的时候抛出,因此我们的try catch用在调用awaite
  • 当我们没有捕捉到异常的时候,这些异常可以使用CoroutineExceptionHandler来捕捉。它是上下文的一个可选参数

你可能感兴趣的:(协程中的取消和异常 (异常处理详解))