Androdi kotlin Coroutines(协程)详解 (一)
Androdi kotlin Coroutines(协程)详解 (二)
Androdi kotlin Coroutines(协程)详解 (三)
Androdi kotlin Coroutines(协程)详解 (四)
Androdi kotlin Coroutines(协程)详解 (五)
Androdi kotlin Coroutines(协程)详解 (六)
6.1 异常的传播
协程构建器有两种形式:自动传播异常(launch 与 actor)或向用户暴露异常(async 与 produce)。 当这些构建器用于创建一个根协程时,即该协程不是另一个协程的子协程, 前者这类构建器将异常视为未捕获异常,类似 Java 的Thread.uncaughtExceptionHandler, 而后者则依赖用户来最终消费异常。
private fun coroutineTryCatch() {
GlobalScope.launch {
try {
val job = launch {
LogUtils.d("Throwing exception from launch")
throw IndexOutOfBoundsException() // 我们将在控制台打印 Thread.defaultUncaughtExceptionHandler
}
job.join()
println("Joined failed job")
val deferred = async { // async 根协程
LogUtils.d("Throwing exception from async")
throw ArithmeticException() // 没有打印任何东西,依赖用户去调用等待
}
deferred.await()
LogUtils.d("Unreached")
} catch (e: Exception) {
LogUtils.d("Caught $e")
}
}
6.2 launch方式tryCatch
private fun launchTryCatch() {
private fun launchTryCatch() {
GlobalScope.launch(Dispatchers.IO) {
try {
val job = launch {
LogUtils.d("Throwing exception from launch")
throw IndexOutOfBoundsException() // 我们将在控制台打印 Thread.defaultUncaughtExceptionHandler
}
job.join()
LogUtils.i("Joined failed job")
} catch (e: Exception) {
LogUtils.e("catch exception $e")
}
}
}
}
E/ExceptionFragment: catch exception kotlinx.coroutines.JobCancellationException: StandaloneCoroutine is cancelling; job=StandaloneCoroutine{Cancelling}@a036e25
--------- beginning of crash
E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-1
Process: com.huang.coroutine, PID: 4966
java.lang.IndexOutOfBoundsException
at com.huang.coroutine.ui.ExceptionFragment$launchTryCatch$1$job$1.invokeSuspend(ExceptionFragment.kt:68)
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)
总结:
- launch方式构建的协程,抛出异常视为未捕获异常,类似 Java 的 Thread.uncaughtExceptionHandler
6.3 async方式tryCatch
private fun asyncTryCatch() {
GlobalScope.launch {
LogUtils.d("async start")
val deferred = async { // async 根协程
try {
LogUtils.d("Throwing exception from async")
throw ArithmeticException() // 没有打印任何东西,依赖用户去调用等待
} catch (e: Exception) {
LogUtils.d("Caught 2 $e")
}
}
deferred.await()
LogUtils.d("Unreached")
}
}
总结:
- 用户暴露异常(async 与 produce),依赖用户来最终消费异常
6.3 CoroutineExceptionHandler全局异常处理者
在 Android 中, uncaughtExceptionPreHandler 被设置在全局协程异常处理者中。
private val handler = CoroutineExceptionHandler { _, exception ->
LogUtils.e("CoroutineExceptionHandler got $exception")
}
private fun exceptionHandler() {
GlobalScope.launch(handler) {
val job = launch {
LogUtils.d("Throwing exception from launch")
throw IndexOutOfBoundsException() // 我们将在控制台打印 Thread.defaultUncaughtExceptionHandler
}
job.join()
LogUtils.d("Joined failed job")
val deferred = async { // async 根协程
LogUtils.d("Throwing exception from async")
throw ArithmeticException() // 没有打印任何东西,依赖用户去调用等待
}
deferred.await()
LogUtils.d("Unreached")
}
}
总结:
- CoroutineExceptionHandler 的实现并不是用于子协程。
- 如果根协程是launch启动,是可以捕获异常,而如果是async方式则捕获不了
6.4 异常聚合
当协程的多个子协程因异常而失败时, 一般规则是“取第一个异常”,因此将处理第一个异常。 在第一个异常之后发生的所有其他异常都作为被抑制的异常绑定至第一个异常。
private fun exceptionTogether() {
GlobalScope.launch(handler) {
LogUtils.d("exception together start")
launch {
try {
delay(500) // 当另一个同级的协程因 IOException 失败时,它将被取消
LogUtils.d("send exception")
} finally {
throw ArithmeticException() // 第二个异常
}
}
launch {
delay(100)
LogUtils.d("first exception")
throw IOException() // 首个异常
}
LogUtils.d("exception together end")
}
}
6.5 监督
可能有以下需求
- 一个 UI 的子作业执行失败了,它并不总是有必要取消(有效地杀死)整个 UI 组件, 但是如果 UI 组件被销毁了(并且它的作业也被取消了),由于它的结果不再被需要了,它有必要使所有的子作业执行失败
- 服务进程孵化了一些子作业并且需要 监督 它们的执行,追踪它们的故障并在这些子作业执行失败的时候重启。
6.5.1 监督作业
它类似于常规的 Job,唯一的不同是:SupervisorJob 的取消只会向下传播。
private fun supervisorJob() {
GlobalScope.launch(Dispatchers.Main) {
val supervisor = SupervisorJob()
with(CoroutineScope(coroutineContext + supervisor)) {
// 启动第一个子作业——这个示例将会忽略它的异常(不要在实践中这么做!)
val firstChild = launch(CoroutineExceptionHandler { _, _ -> }) {
LogUtils.d("The first child is failing")
throw AssertionError("The first child is cancelled")
}
// 启动第二个子作业
val secondChild = launch {
firstChild.join()
// 取消了第一个子作业且没有传播给第二个子作业
LogUtils.d("The first child is cancelled: ${firstChild.isCancelled}, but the second one is still active")
try {
delay(Long.MAX_VALUE)
} finally {
// 但是取消了监督的传播
LogUtils.d("The second child is cancelled because the supervisor was cancelled")
}
}
// 等待直到第一个子作业失败且执行完成
firstChild.join()
LogUtils.d("Cancelling the supervisor")
supervisor.cancel()
secondChild.join()
}
}
}
6.5.2 监督作用域
对于作用域的并发,可以用 supervisorScope 来替代 coroutineScope 来实现相同的目的。它只会单向的传播并且当作业自身执行失败的时候将所有子作业全部取消。作业自身也会在所有的子作业执行结束前等待, 就像 coroutineScope 所做的那样。
private fun supervisorScope() {
GlobalScope.launch {
try {
supervisorScope {
val child = launch {
try {
LogUtils.d("The child is sleeping")
delay(Long.MAX_VALUE)
} finally {
LogUtils.d("The child is cancelled")
}
}
// 使用 yield 来给我们的子作业一个机会来执行打印
yield()
LogUtils.d("Throwing an exception from the scope")
throw AssertionError()
}
} catch (e: AssertionError) {
LogUtils.d("Caught an assertion error")
}
}
6.5.3 监督协程中的异常
监督协程中的每一个子作业应该通过异常处理机制处理自身的异常。 这种差异来自于子作业的执行失败不会传播给它的父作业的事实。 这意味着在 supervisorScope 内部直接启动的协程确实使用了设置在它们作用域内的CoroutineExceptionHandler,与父协程的方式相同
private fun supervisorException() {
GlobalScope.launch {
val handler = CoroutineExceptionHandler { _, exception ->
LogUtils.d("CoroutineExceptionHandler got $exception")
}
supervisorScope {
val child = launch(handler) {
LogUtils.d("The child throws an exception")
throw AssertionError()
}
LogUtils.d("The scope is completing")
}
LogUtils.d("The scope is completed")
}
}