kotlin协程四

前言

翻译自 协程异常
这一章节将介绍在协程中异常是如何传播的,以及如何通过不同的方法处理他们。

协程突然失败了,怎么办

如果一个协程exception了,会将上述异常传给它的父级。之后,父级会1.取消其他的子级2. 取消自己3.传递异常给他的父级
这个异常会传达到根级,所有这个scope开始的协程都会被取消。



尽管在某些情况下传播异常可能很有意义,但也有一些不期望传异常的。想象一个专门处理用户操作的与UI有关的scope。如果一个子协程抛出异常,UI scope也会被取消,整个UI 组件都会没有反应,因为一个取消掉的scope不能再开始更多的协程.
那该怎么办呢?或者,可以在创建这些协程的CoroutineScope的CoroutineContext中使用Job的不同实现,即SupervisorJob。

SupervisorJob拯救

使用SupervisorJob ,child的失败将不会影响其他的child。SupervisorJob不会取消自己或者其他的子级。而且,SupervisorJob不会传递异常,并且允许child去处理异常。
你可以用类似val uiScope = CoroutineScope(SupervisorJob())创建一个scope,这样当一个协程失败了,也不会去传递cancellation.


如果这个异常没有处理,并且CoroutineContext没有CoroutineExceptionHandler(后续会讲),它将会传到默认的线程ExceptionHandler。在虚拟机中,这个异常会被打印在console中,在安卓中,它会让你的APP crash,无论是在哪个dispatcher上发生的。
note: 没有被拦截的异常总会被抛出,不管你用的哪种job。
同样的行为适用于coroutinescope和supervisorscope。这些将创建一个子scope(相应的job或SupervisorJob作为父级),这样你可以有逻辑的组织协程。(例如。做并行的计算或者让他们互相影响或互不影响)
注意:仅当supervisorJob作为scope的一部分才可以像描述的那样:使用supervisorScope或CoroutineScope(SupervisorJob())创建

Job还是SupervisorJob?

用SupervisorJob 或者supervisorScope当你不想要失败的时候去取消父级和同级。
例如

val scope = CoroutineScope(SupervisorJob())
scope.launch {
    // Child 1
}
scope.launch {
    // Child 2
}

在这种情况下,如果child1失败了,不会取消child2和scope。
例如,

val scope = CoroutineScope(Job())
scope.launch {
    supervisorScope {
        launch {
            // Child 1
        }
        launch {
            // Child 2
        }
    }
}

这个例子,supervisorScope用supervisorJob创建了一个子scope,如果child1失败了,child2不会被取消。
相反,如果使用coroutineScope,失败将会传递并且也会取消掉scope。

谁是我的父级?

你能辨别出下面的代码中谁是child1的父级吗?

val scope = CoroutineScope(Job())
scope.launch(SupervisorJob()) {
    // new coroutine -> can suspend
   launch {
        // Child 1
    }
    launch {
        // Child 2
    }
}

child1的父级是job类型,即使第一印象你觉得是supervisorjob类型,但那是不对的。不是因为一个新的协程总会创建一个新的job,而是因为在这种情况下,会覆盖掉supervisorjob。supervisorjob是scope.launch创建的协程的父级,SupervisorJob在这段代码里啥也没干,也就是没啥用。


child1和child2的父级都是job类型,不是supervisorjob

因此,无论child1.还是child2失败,这个失败都会到达scope,所有被这个scope开始的工作都会被cancel掉。
记住supervisorjob只有在使用supervisorScope或者CoroutineScope(SupervisorJob())才有用 。作为创建协程的参数的supervisorJob并不会有你期待的cancellation作用。
对于异常,如果一个子级抛出异常,supervisorjob不会传递异常,并允许自己的协程去处理它。

幕后工作

如果你好奇job的幕后工作感到好奇,可以查看JobSupport.kt文件的childCancelled和notifycabcelling方法。
在SupervisorJob的实现类中,childCancelled返回了false,这意味着它不会传递取消但也不处理异常。

异常处理

协程使用常规的Kotlin语法来处理异常:try / catch或内置帮助程序功能(如runCatching(内部使用try / catch))
我们之前说过总会抛出不被捕获的异常。然而,不同协程构建器用不同的方式处理异常。

Launch

异常会在发生的时候立刻抛出。因此,可以这样子

scope.launch {
    try {
        codeThatCanThrowExceptions()
    } catch(e: Exception) {
        // Handle exception
    }
}
Async

当async用作根协程时(是CoroutineScope实例或supervisorScope的直接子级),异常不会立刻抛出,而是在调用.await()时抛出。例如

supervisorScope {
    val deferred = async {
        codeThatCanThrowExceptions()
    }
    try {
        deferred.await()
    } catch(e: Exception) {
        // Handle exception thrown in async
    }
}

在这个示例中,注意调用async永远不会抛出异常,await才会抛出async协程的异常。
注意,我们使用的是supervisorScope去调用async和await。就像我们之前说的,supervisorjob允许协程去处理异常。相反的,如果用job将会自动将异常向上传递,因此catch块无用。

coroutineScope {
    try {
        val deferred = async {
            codeThatCanThrowExceptions()
        }
        deferred.await()
    } catch(e: Exception) {
        // 异常永远不会被这里捕获,而是会向上传给scope
    }
}

此外,通过其他协程创建的协程发生异常,一定会被传递,无论使用的哪种构造器。例如

val scope = CoroutineScope(Job())
scope.launch {
    async {
        // 如果async抛出异常,launch会抛出而不需要调用.await()
    }
}

在这个例子中,如果async会在异常发生的时候立刻抛出异常,因为协程是scope launch的直接子级。因为async(job在它自己的CoroutineContext中)会自动的将异常传给它的父级(launch)来抛出异常。
注意:在CoroutineScope构造器中或通过其他协程创建的协程抛出的异常都不能被try/catch捕获
在SupervisorJob小节中,我们提到了CoroutineExceptionHandler的存在。接下来让我们深入研究它。

CoroutineExceptionHandler

CoroutineExceptionHandler是CoroutineContext的一种可选择元素,允许你处理未捕获的异常。
下面是定义CoroutineExceptionHandler的示例,无论何时捕获到一个异常,你都会有关于CoroutineContext和异常本身的相关信息。

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

如果满足一下要求,exception会被捕获:
when: 协程会自动抛出异常(launch,而非async)
where: 在CououtineScope的CoroutineContext中或根协程(CoroutineScope或supervisorScope的直接子级)
在下面这个例子中,exception会被handler捕获

val scope = CoroutineScope(Job())
scope.launch(handler) {
    launch {
        throw Exception("Failed coroutine")
    }
}

而在下面这个例子中,exception不会被捕获

val scope = CoroutineScope(Job())
scope.launch {
    launch(handler) {
        throw Exception("Failed coroutine")
    }
}

因为这个handler没有被放置在正确的CoroutineContext中。内置的launch会立刻传递异常给父级,父级不会知道关于这个handler一无所知,异常会被抛出。

优雅的处理异常是很重要的,记住用SupervisorJob当你想在异常发生时避免传递cancellation,否则用job。

后记

下一节学习不应该被取消的工作

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

推荐阅读更多精彩内容