来个面试题,看看你对 kotlin coroutine掌握得如何?

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 来个面试题,看看你对 kotlin coroutine掌握得如何?

给出下面代码:

lifecycleScope.launch(Dispatchers.IO) {
    val task1 = async {
        throw RuntimeException("task1 failed")
    }
    val task2 = async {
        throw RuntimeException("task2 failed")
    }
    try {
        task1.await()
    } catch (e: Throwable){
        Log.i("test", "catch task1: $e")
    }
    Log.i("test", "is coroutine active: $isActive")
    try {
        task2.await()
    } catch (e: Throwable){
        Log.i("test", "catch task2: $e")
    }
    Log.i("test", "scope end.")
}

问:app 会发生什么?输出的日志是怎样子的?为什么?

......

......

......

......

......

......

......

......

......

答:app 会 crash,输出日志为

I/test: catch task1: java.lang.RuntimeException: task1 failed
I/test: is coroutine active: false
I/test: catch task2: kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job=DeferredCoroutine{Cancelling}
I/test: scope end.

魔幻吗?


那我们就来分析下为啥结果是这个样子的。


协程有一个很基础的设定:默认情况下,异常会往外层 scope 抛,用以立刻取消外层 scope 内的其它的子 job。


在上面的例子中,假设:lifecycleScope.launch 创建的子 scope 为 A。task1 用 async 创建 scope A 的子 scope 为 B。task2 用 async 创建 scope A 的子 scope 为 C。


当 scope B 发生异常,scope B 会将异常抛给 scope A,scope A 会 cancel 掉 task2 和自己,再把异常抛给 lifecycleScope,因为 lifecycleScope 没有 CoroutineExceptionHandler 并且 scope A 是通过 launch 启动的,所以 crash 就发生了。


那如何打断异常的这个传播链呢?


答案就是使用 SupervisorJob,或者用基于它的 supervisorScope。它不会把异常往上抛,也不会取消掉其它的子 job。但是,SupervisorJob 对 launch 和 async 启动的协程的态度是不一样的,它的源码注释里写明了的,简单的认为它会吃掉异常是会踩坑的。

263c422b7db4b039ed515f7c12417b1.png

翻译出来就是,如果是 launch 启动的子协程,是需要 CoroutineExceptionHandler 配合处理的,如果是 async 启动的协程,就是真的不抛,等到 Deferred.await 时再抛。


所以,在上面的代码中,虽然 lifecycleScope 有用到 SupervisorJob,但异常从 scopeA 往上传时,因为没有 CoroutineExceptionHandler,所以跪了。


那么为什么 async 要往上抛异常,导致 await 的 try catch 还需要 supervisorScope 的配合?感觉有点反人类?


想象一下下面的场合:

lifecycleScope.launch {
    val task1 = async { "非常耗时的操作,但没有异常" }
    val task2 = async { throw RuntimeException("") }
    val result1 = task1.await()
    val result2 = task2.await()
}

因为 task2 有异常,所以整个协程必定会失败。如果等 await 时才跑错误, 那么就需要等耗时的 task1 执行完成,轮到 task 的 await 调用时,异常才能跑出来,虽然也没啥问题,就是白白耗费了 task1 的执行。


而依据当前的设计,task2 抛出异常,那么外层 scope 就会把 task1 也给取消了,整个 scope 也就执行结束了。async 源码里提到的原因是为了 structured concurrency,也是期望使用者更多的关注 scope 以及 scope 内各个任务的关联关系吧。不过这坑确实有点让人有时摸不着头脑,可能以后就变了也说不定。


剩下一个问题是,task1 失败后就往上抛吗?为啥 catch task1 后还有日志打印出来?


其实上面已经提到了,异常抛给 scope A 后,它会 cancel 掉自己,再往上抛,而 cancel 掉自己并不是强制终止掉协程的执行,而是先变更状态为 cancelling,所以日志中 isActive 已经变成 false 了,第二个异常也不是 task2 的异常,而是 await 本身抛出的 CancellationException。这里告诉我们要注意两点:


1.try catch 时如果是 CancellationException,要记得 rethrow。

2.一些循环、耗时的点,要记得用 isActive 或者 ensureActive 检查,不要写出不能正常 cancel 的协程。像 delay 等 api,官方已经做好了这方面的检查,极大地方便了开发者。这个线程 interrupted 相关知识点是同一个道理。


了解了各种坑点以及背后的原因,我们就可以把协程用得飞起了。最后,修复文章开头提到的问题,就是简单包个 supervisorScope 就行啦。

lifecycleScope.launch(Dispatchers.IO) {
    supervisorScope {
        val task1 = async {
            throw RuntimeException("task1 failed")
        }
        val task2 = async {
            throw RuntimeException("task2 failed")
        }
        try {
            task1.await()
        } catch (e: Throwable){
            Log.i("test", "catch task1: $e")
        }
        Log.i("test", "is coroutine active: $isActive")
        try {
            task2.await()
        } catch (e: Throwable){
            Log.i("test", "catch task2: $e")
        }
        Log.i("test", "scope end.")
    }
}
相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
1月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
**Kotlin中的`by lazy`和`lateinit`都是延迟初始化技术。`by lazy`用于只读属性,线程安全,首次访问时初始化;`lateinit`用于可变属性,需手动初始化,非线程安全。`by lazy`支持线程安全模式选择,而`lateinit`适用于构造函数后初始化。选择依赖于属性特性和使用场景。**
67 5
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
|
28天前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin中常见作用域函数
**Kotlin作用域函数概览**: `let`, `run`, `with`, `apply`, `also`. `let`安全调用并返回结果; `run`在上下文中执行代码并返回结果; `with`执行代码块,返回结果; `apply`配置对象后返回自身; `also`附加操作后返回自身
26 8
|
1月前
|
SQL 安全 Java
Android经典面试题之Kotlin中object关键字实现的是什么类型的单例模式?原理是什么?怎么实现双重检验锁单例模式?
Kotlin 单例模式概览 在 Kotlin 中,`object` 关键字轻松实现单例,提供线程安全的“饿汉式”单例。例如: 要延迟初始化,可使用 `companion object` 和 `lazy` 委托: 对于参数化的线程安全单例,结合 `@Volatile` 和 `synchronized`
29 6
|
1月前
|
Android开发 Kotlin
Android面试题之kotlin中怎么限制一个函数参数的取值范围和取值类型等
在Kotlin中,限制函数参数可通过类型系统、泛型、条件检查、数据类、密封类和注解实现。例如,使用枚举限制参数为特定值,泛型约束确保参数为Number子类,条件检查如`require`确保参数在特定范围内,数据类封装可添加验证,密封类限制为一组预定义值,注解结合第三方库如Bean Validation进行校验。
37 6
|
1月前
|
Android开发 Kotlin
Android面试题之 Kotlin中退出迭代器的方式有哪些
在Android和Kotlin中,遍历集合时可使用迭代器结合`break`提前终止循环。例如,使用`while`和迭代器,或用`forEach`配合`return@forEach`来中断遍历。若需退出外层函数,可定义自定义标签。在遍历并删除元素时,这些技巧尤其有用。
26 3
|
1月前
|
Android开发 Kotlin
Android经典面试题之Kotlin中Lambda表达式有哪些用法
Kotlin的Lambda表达式是匿名函数的简洁形式,常用于集合操作和高阶函数。基本语法是`{参数 -> 表达式}`。例如,`{a, b -> a + b}`是一个加法lambda。它们可在`map`、`filter`等函数中使用,也可作为参数传递。单参数时可使用`it`关键字,如`list.map { it * 2 }`。类型推断简化了类型声明。
14 0
|
1月前
|
Android开发 Kotlin
Android经典面试题之Kotlin中Lambda表达式和匿名函数的区别
**Kotlin中的匿名函数与Lambda表达式概述:** 匿名函数(`fun`关键字,明确返回类型,支持非局部返回)适合复杂逻辑,而Lambda(简洁语法,类型推断)常用于内联操作和高阶函数参数。两者在语法、返回类型和使用场景上有所区别,但都提供无名函数的能力。
15 0
|
3天前
|
存储 缓存 网络协议
复盘女朋友面试4个月的Java基础题
这篇文章是关于Java基础面试题的复盘,涵盖了HashMap原理、对象序列化作用等高频面试问题,并强调了Java基础知识的重要性。
复盘女朋友面试4个月的Java基础题
|
5天前
|
存储 NoSQL Java
一天五道Java面试题----第十一天(分布式架构下,Session共享有什么方案--------->分布式事务解决方案)
这篇文章是关于Java面试中的分布式架构问题的笔记,包括分布式架构下的Session共享方案、RPC和RMI的理解、分布式ID生成方案、分布式锁解决方案以及分布式事务解决方案。
一天五道Java面试题----第十一天(分布式架构下,Session共享有什么方案--------->分布式事务解决方案)
|
29天前
|
SQL Java Unix
Android经典面试题之Java中获取时间戳的方式有哪些?有什么区别?
在Java中获取时间戳有多种方式,包括`System.currentTimeMillis()`(毫秒级,适用于日志和计时)、`System.nanoTime()`(纳秒级,高精度计时)、`Instant.now().toEpochMilli()`(毫秒级,ISO-8601标准)和`Instant.now().getEpochSecond()`(秒级)。`Timestamp.valueOf(LocalDateTime.now()).getTime()`适用于数据库操作。选择方法取决于精度、用途和时间起点的需求。
32 3