【译】kotlin 协程官方文档(7)- 异常处理

最近一直在了解关于 Kotlin协程 的知识,那最好的学习资料自然是官方提供的学习文档了,看了看后我就萌生了翻译官方文档的想法。前后花了要接近一个月时间,一共九篇文章,在这里也分享出来,希望对读者有所帮助。个人知识所限,有些翻译得不是太顺畅,也希望读者能提出意见

协程官方文档:coroutines-guide

本节讨论协程关于异常的处理和取消异常。我们已经知道,取消协程会使得在挂起点抛出 CancellationException,而协程机制会忽略这个异常。但是,如果在取消期间抛出异常,或者协程的多个子协程抛出异常,此时会发生什么情况呢?

一、异常的传播

协程构建器有两种类型:自动传播异常(launch 和 actor)和向用户公开异常(async 和 product)。前者将异常视为未捕获异常,类似于 Java 的 Thread.uncaughtExceptionHandler,而后者则需要由开发者自己来处理最终的异常,例如通过 await 或 receive(product 和 receive 在 Channels 章节介绍)

可以通过在 GlobalScope 创建协程的简单示例来演示:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = GlobalScope.launch {
        println("Throwing exception from launch")
        throw IndexOutOfBoundsException() // Will be printed to the console by Thread.defaultUncaughtExceptionHandler
    }
    job.join()
    println("Joined failed job")
    val deferred = GlobalScope.async {
        println("Throwing exception from async")
        throw ArithmeticException() // Nothing is printed, relying on user to call await
    }
    try {
        deferred.await()
        println("Unreached")
    } catch (e: ArithmeticException) {
        println("Caught ArithmeticException")
    }
}

运行结果:

Throwing exception from launch
Exception in thread "DefaultDispatcher-worker-2 @coroutine#2" java.lang.IndexOutOfBoundsException
Joined failed job
Throwing exception from async
Caught ArithmeticException

二、CoroutineExceptionHandler

如果不想将所有的异常都打印到控制台上,CoroutineExceptionHandler 上下文元素可以作为协程全局通用的 catch 块,在这里进行自定义日志记录或异常处理。它类似于对线程使用 Thread.uncaughtExceptionHandler

在 JVM 上,可以通过 ServiceLoader 注册 CoroutineExceptionHandler 来重新定义所有协程的全局异常处理器。全局异常处理程序类似于 Thread.defaultUncaughtExceptionHandler ,后者在没有注册其它特定处理程序时使用。在 Android 上,uncaughtExceptionPreHandler 作为全局协程异常处理程序存在

CoroutineExceptionHandler 只在预计不会由用户处理的异常上调用,因此在 async 这类协程构造器中注册它没有任何效果

import kotlinx.coroutines.*

fun main() = runBlocking {
    //sampleStart
    val handler = CoroutineExceptionHandler { _, exception -> 
        println("Caught $exception") 
    }
    val job = GlobalScope.launch(handler) {
        throw AssertionError()
    }
    val deferred = GlobalScope.async(handler) {
        throw ArithmeticException() // Nothing will be printed, relying on user to call deferred.await()
    }
    joinAll(job, deferred)
    //sampleEnd    
}

运行结果:

Caught java.lang.AssertionError

三、取消和异常

取消和异常是紧密联系的。协程在内部使用 CancellationException 来进行取消,所有处理程序都会忽略这类异常,因此它们仅用作调试信息的额外来源,这些信息可以用 catch 块捕获。当使用 Job.cancel 取消协程时,协程将停止运行,但不会取消其父协程

import kotlinx.coroutines.*

fun main() = runBlocking {
    //sampleStart
    val job = launch {
        val child = launch {
            try {
                delay(Long.MAX_VALUE)
            } finally {
                println("Child is cancelled")
            }
        }
        yield()
        println("Cancelling child")
        child.cancel()
        child.join()
        yield()
        println("Parent is not cancelled")
    }
    job.join()
    //sampleEnd    
}

运行结果:

Cancelling child
Child is cancelled
Parent is not cancelled

如果协程遇到 CancellationException 以外的异常,它将用该异常取消其父级。无法重写此行为,它用于为不依赖于 CoroutineExceptionHandler 实现的结构化并发,为之提供稳定的协程层次结构。当父级的所有子级终止时,父级将处理原始异常

这也是为什么在这些示例中,总是将 CoroutineExceptionHandler 作为参数传递给在 GlobalScope 中创建的协程中的原因。将 CoroutineExceptionHandler 设置给主 runBlocking 范围内启动的协程是没有意义的,因为尽管设置了异常处理器,主协程在其子级异常抛出后仍将被取消

import kotlinx.coroutines.*

fun main() = runBlocking {
    //sampleStart
    val handler = CoroutineExceptionHandler { _, exception -> 
        println("Caught $exception") 
    }
    val job = GlobalScope.launch(handler) {
        launch { // the first child
            try {
                delay(Long.MAX_VALUE)
            } finally {
                withContext(NonCancellable) {
                    println("Children are cancelled, but exception is not handled until all children terminate")
                    delay(100)
                    println("The first child finished its non cancellable block")
                }
            }
        }
        launch { // the second child
            delay(10)
            println("Second child throws an exception")
            throw ArithmeticException()
        }
    }
    job.join()
    //sampleEnd    
}

运行结果:

Second child throws an exception
Children are cancelled, but exception is not handled until all children terminate
The first child finished its non cancellable block
Caught java.lang.ArithmeticException

CoroutineExceptionHandler 将等到所有子协程运行结束后再回调。second child 抛出异常后将联动导致 first child 结束运行,之后再将异常交予 CoroutineExceptionHandler 处理

四、异常聚合

如果一个协程的多个子协程抛出异常会发生什么情况呢?一般的规则是“第一个异常会获胜”,因此第一个抛出的异常将传递给异常处理器进行处理,但这也有可能会导致异常丢失。例如,如果在某个协程在抛出异常后,第二个协程在其 finally 块中抛出异常,此时第二个协程的异常将不会传递给 CoroutineExceptionHandler

其中一个解决方案是分别抛出每个异常。await 应该有相同的机制来避免行为不一致,这将导致协程的实现细节(无论它是否将部分工作委托给其子级)泄漏给其异常处理器

import kotlinx.coroutines.*
import java.io.*

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("Caught $exception with suppressed ${exception.suppressed.contentToString()}")
    }
    val job = GlobalScope.launch(handler) {
        launch {
            try {
                delay(Long.MAX_VALUE)
            } finally {
                throw ArithmeticException()
            }
        }
        launch {
            delay(100)
            throw IOException()
        }
        delay(Long.MAX_VALUE)
    }
    job.join()  
}

注意:以上代码只能在支持 suppressed exceptions 的 JDK7+ 版本上正常运行

运行结果:

Caught java.io.IOException with suppressed [java.lang.ArithmeticException]

导致协程停止的异常在默认情况下是会被透传,不会被包装的

import kotlinx.coroutines.*
import java.io.*

fun main() = runBlocking {
    //sampleStart
    val handler = CoroutineExceptionHandler { _, exception ->
        println("Caught original $exception")
    }
    val job = GlobalScope.launch(handler) {
        val inner = launch {
            launch {
                launch {
                    throw IOException()
                }
            }
        }
        try {
            inner.join()
        } catch (e: CancellationException) {
            println("Rethrowing CancellationException with original cause")
            throw e
        }
    }
    job.join()
    //sampleEnd    
}

运行结果:

Rethrowing CancellationException with original cause
Caught original java.io.IOException

即使捕获到了 inner 被取消的异常信息,但最终传递给 CoroutineExceptionHandler 的还是 inner 内部真实的异常信息

五、Supervision

正如我们之前所研究的,取消是一种双向关系,会在整个协程层次结构中传播。但如果需要单向取消呢?

此类需求的一个很好的例子在某个范围内定义了 Job 的 UI 组件。如果 UI 组件的任意一个子任务失败了,此时并不一定需要取消(实际上是终止)整个 UI 组件。但是如果 UI 组件的生命周期结束了(并且取消了它的 Job),那么就必须取消所有子 Job, 因为它们的结果不再是必需的

另一个例子是一个服务器进程,它生成几个子 Job 并且需要监测它们的执行,跟踪它们的失败时机,并且仅重新启动那么失败的子 Job

5.1、SupervisorJob

出于这些目的,可以使用 SupervisorJob。它类似于常规的 Job,唯一的例外是取消操作只向下传播。用一个例子很容易演示:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val supervisor = SupervisorJob()
    with(CoroutineScope(coroutineContext + supervisor)) {
        // launch the first child -- its exception is ignored for this example (don't do this in practice!)
        val firstChild = launch(CoroutineExceptionHandler { _, _ -> }) {
            println("First child is failing")
            throw AssertionError("First child is cancelled")
        }
        // launch the second child
        val secondChild = launch {
            firstChild.join()
            // Cancellation of the first child is not propagated to the second child
            println("First child is cancelled: ${firstChild.isCancelled}, but second one is still active")
            try {
                delay(Long.MAX_VALUE)
            } finally {
                // But cancellation of the supervisor is propagated
                println("Second child is cancelled because supervisor is cancelled")
            }
        }
        // wait until the first child fails & completes
        firstChild.join()
        println("Cancelling supervisor")
        supervisor.cancel()
        secondChild.join()
    }
}

运行结果:

First child is failing
First child is cancelled: true, but second one is still active
Cancelling supervisor
Second child is cancelled because supervisor is cancelled

5.2、supervisorScope

对于作用域并发,可以使用 supervisorScope 代替 coroutineScope 来实现相同的目的。它只在一个方向上传播取消操作,并且仅在自身失败时才取消所有子级。它也像 coroutineScope 一样在结束运行之前等待所有的子元素结束运行

import kotlin.coroutines.*
import kotlinx.coroutines.*

fun main() = runBlocking {
    try {
        supervisorScope {
            val child = launch {
                try {
                    println("Child is sleeping")
                    delay(Long.MAX_VALUE)
                } finally {
                    println("Child is cancelled")
                }
            }
            // Give our child a chance to execute and print using yield 
            yield()
            println("Throwing exception from scope")
            throw AssertionError()
        }
    } catch(e: AssertionError) {
        println("Caught assertion error")
    }
}

输出结果:

Child is sleeping
Throwing exception from scope
Child is cancelled
Caught assertion error

以下例子展示了 supervisorScope 中取消操作的单向传播性,子协程的异常不会导致其它子协程取消

fun main() = runBlocking {
    supervisorScope {
        val child1 = launch {
            try {
                for (time in 1..Long.MAX_VALUE) {
                    println("Child 1 is printing: $time")
                    delay(1000)
                }
            } finally {
                println("Child 1 is cancelled")
            }
        }
        val child2 = launch {
            println("Child 2 is sleeping")
            delay(3000)
            println("Child 2 throws an exception")
            throw AssertionError()
        }
    }
}

运行结果:

Child 1 is printing: 1
Child 2 is sleeping
Child 1 is printing: 2
Child 1 is printing: 3
Child 1 is printing: 4
Child 2 throws an exception
Exception in thread "main" java.lang.AssertionError
Child 1 is printing: 5
Child 1 is printing: 6
······

5.3、监督协程中的异常

常规 job 和 supervisor job 的另一个重要区别在于异常处理。每个子级都应该通过异常处理机制自己处理其异常。这种差异来自于这样一个事实:supervisorScope 中子元素的失败不会传导给父级

import kotlin.coroutines.*
import kotlinx.coroutines.*

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception -> 
        println("Caught $exception") 
    }
    supervisorScope {
        val child = launch(handler) {
            println("Child throws an exception")
            throw AssertionError()
        }
        println("Scope is completing")
    }
    println("Scope is completed")
}

运行结果:

Scope is completing
Child throws an exception
Caught java.lang.AssertionError
Scope is completed
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351

推荐阅读更多精彩内容