开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第21天,点击查看活动详情
1.cancel()不能被响应
fun catchTest() = runBlocking {
val job = launch(Dispatchers.IO) {
var i = 0
while (true) {
i++
}
}
job.log()
delay(2000L)
job.log()
job.cancel()
job.log()
println("End")
}
fun Job.log() {
println(
"""
isActive:$isActive
isCompleted:$isCompleted
isCancelled:$isCancelled
Thread:${Thread.currentThread().name}
================================
""".trimIndent()
)
}
//输出结果:
//isActive:true
//isCompleted:false
//isCancelled:false
//Thread:main
//================================
//isActive:true
//isCompleted:false
//isCancelled:false
//Thread:main
//================================
//isActive:false
//isCompleted:false
//isCancelled:true
//Thread:main
//================================
//End
上面的代码通过launch
启动了一个协程,在协程中执行一个死循环,2000毫秒后取消任务的执行,isCancelled = true
说明job确实被取消了,但是从代码运行结果来看这个job并没有被结束,换句话说就是cancel()
没有起作用,这是为什么呢?分析其原因就是因为调用cancel()
后协程任务已经变得不在活跃了,但是代码并没有把isActive
作为循环条件,因此此任务不能被取消。
之前也有聊到过,协程就是互相协作的程序,在外部取消任务的同时内部其实也是需要做出响应的,具体来说就是要在内部加入isActive
的判断,通过协程的运行状态决定是否继续执行任务:
fun catchTest() = runBlocking {
val job = launch(Dispatchers.IO) {
var i = 0
// 变化在这里
// ↓
while (isActive) {
i++
}
}
job.log()
delay(2000L)
job.log()
job.cancel()
job.log()
println("End")
}
//输出结果:
//isActive:true
//isCompleted:false
//isCancelled:false
//Thread:main
//================================
//isActive:true
//isCompleted:false
//isCancelled:false
//Thread:main
//================================
//isActive:false
//isCompleted:false
//isCancelled:true
//Thread:main
//================================
//End
//
//Process finished with exit code 0
加入isActive
的循环后任务就可以正常取消了,这个异常用一句话总结:协程的在外部能顺利取消的必要条件是需要内部的配合。
2.结构被破坏
协程是具有结构化的,当父协程被取消时子协程也会跟着被取消,但是总有个例外不是,这个例外就是结构被破坏。
val myFixedDispatcher = Executors.newFixedThreadPool(3) {
Thread(it, "myFixedDispatcher").apply { isDaemon = false }
}.asCoroutineDispatcher()
fun catchTest() = runBlocking {
val parentLaunch = launch(myFixedDispatcher) {
launch(Job()) {
var i = 0
while (isActive) {
delay(1000L)
i++
println("launch1 i: $i")
}
}
launch {
var i = 0
while (isActive) {
delay(1000L)
i++
println("launch2 i: $i")
}
}
launch {
var i = 0
while (isActive) {
delay(1000L)
i++
println("launch3 i: $i")
}
}
}
delay(2000L)
parentLaunch.cancel()
println("Process end!")
}
//输出结果:
//launch1 i: 1
//launch2 i: 1
//launch3 i: 1
//Process end!
//launch1 i: 2
//launch1 i: 3
//launch1 i: 4
//launch1 i: 5
//launch1 i: 6
//launch1 i: 7
上面的代码定义了一个parentLaunch,在它的里面又定义了三个launch,这是一个结构化的代码,按照之前在Job
中聊过的一些信息可以知道当parentLaunch调用cancel()
方法后子协程都会停止执行,但是在第一个launch中加入了一个Job,就导致第一个子launch一直在运行了,这主要是因为原本和谐的结构被破坏了,画张图对比一下:
从两张对比图可以看出,结构被破坏的launch已经不属于parentLaunch的子job了,所以也就可以理解当parentLaunch取消后launch1还在执行任务。而如果想要正常执行则要删除launch1的Job即可。
通过上面的分析可以得出一个结论:不要轻易打破协程的父子结构。
3.异常捕获后要抛出来
前面在聊协程的并发操作——Mutex时特地弄出一个i / 0的异常,同时也将其捕获了,但是程序并没有停止而是卡住了,原因就是异常被捕获后没有抛出
fun concurrentTest() = runBlocking {
val time = measureTimeMillis {
val jobs = mutableListOf<Job>()
var i = 0
val mutex = Mutex()
repeat(10) {
val job = launch(Dispatchers.Default) {
repeat(1000) {
try {
mutex.lock()
i++
i / 0
mutex.unlock()
} catch (e: Exception) {
e.printStackTrace()
throw e //变化在这里
}
}
}
jobs.add(job)
}
jobs.joinAll()
println("i: $i")
}
println("time: $time")
}
上面的代码就是Mutex的案例,只是在捕获之后加了throw e
程序就可以停止执行了。所以这里还可以得出一个结论:捕获异常后要考虑是否要将异常抛出。这里还有一点要注意,try-catch
的异常用的是Exception
,它是所有异常的父类,用它的话会影响效率,正确的应该是使用指定异常,这在Java中也是要这么做的,那么上面的Exception
就可以修改为ArithmeticException
4.协程中的try-catch失效
在Java开发中有时候会用try-catch
将代码都包起来,那么这样的做法在协程中是否有效呢
上面的代码简化一下,来验证一下try-catch
是否真的失效了
fun catchTest() = runBlocking {
var i = 0
var i = 0
try {
launch {
i++
i / 0
}
} catch (e: ArithmeticException) {
println("catch: $e")
}
println("end")
}
//输出结果:
//end
java.lang.ArithmeticException: / by zero
异常没有被捕获了且程序报错,在用async
试试
fun catchTest() = runBlocking {
var i = 0
try {
val deferred = async {
i++
i / 0
}
deferred.await()
} catch (e: ArithmeticException) {
println("catch: $e")
}
println("end")
}
java.lang.ArithmeticException: / by zero
//输出结果:
//end
//catch: java.lang.ArithmeticException: / by zero
async
依旧会报错,还有个疑问,已知async
只有调用了await
才会返回结果,那是不是可以理解为不调用await
就不会报错呢?
fun catchTest() = runBlocking {
var i = 0
try {
val deferred = async {
i++
i / 0
}
// deferred.await()
} catch (e: ArithmeticException) {
println("catch: $e")
}
println("end")
}
//输出结果:
//catch: java.lang.ArithmeticException: / by zero
//end
java.lang.ArithmeticException: / by zero
依旧会报错,那么为什么try-catch
会失效呢?这主要是跟代码执行顺序有关,因为,当协程体当中的“i / 0”执行的时候,我们的程序已经跳出 try-catch
的作用域了。
5.SupervisorJob
public fun SupervisorJob(parent: Job? = null) : CompletableJob =
SupervisorJobImpl(parent)
public interface CompletableJob : Job {
public fun complete(): Boolean
public fun completeExceptionally(exception: Throwable): Boolean
}
SupervisorJob
是一个顶层函数,返回一个Job
的子类,它与Job
的区别就在于当父Job
中的某个子Job
发生异常的时候其他的子Job
不会受到牵连。用两段代码做个对比:
- 没有使用SupervisorJob
fun catchTest() = runBlocking {
val parentJob = launch {
launch {
println("satrt launch1")
delay(1000L)
1 / 0 //刻意制造一个错误
println("end launch1")
}
launch {
println("satrt launch2")
delay(1000L)
println("end launch2")
}
launch {
println("satrt launch3")
delay(1000L)
println("end launch3")
}
}
delay(2000L)
}
//输出结果:
//satrt launch1
//satrt launch2
//satrt launch3
Exception in thread "main" java.lang.ArithmeticException: / by zero
Process finished with exit code 0
上面这段代码是没有使用SupervisorJob
的,从输出日志可以看到当launch1出现错误后程序会终止,后面的launch2、launch3的任务也会跟着终止。
- 使用SupervisorJob
fun catchTest() = runBlocking {
val scope = CoroutineScope(SupervisorJob())
scope.launch {
launch {
println("satrt launch1")
delay(1000L)
1 / 0 //刻意制造一个错误
println("end launch1")
}
launch {
println("satrt launch2")
delay(1000L)
println("end launch2")
}
launch {
println("satrt launch3")
delay(1000L)
println("end launch3")
}
}
delay(2000L)
}
//输出结果:
//satrt launch1
//satrt launch2
//satrt launch3
//end launch2
//end launch3
Exception in thread "DefaultDispatcher-worker-2 @coroutine#3" java.lang.ArithmeticException: / by zero
Process finished with exit code 0
上面这段代码是使用了SupervisorJob
的,从输出日志可以看到当launch1出现错误后程序并不会终止,后面的launch2、launch3的任务会执行完毕。
这里又得出一条结论:合理使用SupervisorJob
可提高代码执行完成度,降低异常传播范围。 流程图表示如下
6.CoroutineExceptionHandler
前面总结了几种异常处理办法,都是针对某一个具体的异常处理,如果在复杂的业务场景中这几种方式处理起来就比较麻烦了,此时就要使用Kotlin提供的CoroutineExceptionHandler异常处理方案,现在修改一下上面使用了SupervisorJob()
的代码,给它多加几个错误:
fun catchTest() = runBlocking {
val scope = CoroutineScope(SupervisorJob())
val parentJob = scope.launch {
launch {
println("satrt launch1")
delay(1000L)
1 / 0 //刻意制造一个错误
println("end launch1")
}
launch {
println("satrt launch2")
delay(1000L)
1 / 0 //刻意制造一个错误
println("end launch2")
}
launch {
println("satrt launch3")
delay(1000L)
1 / 0 //刻意制造一个错误
println("end launch3")
}
}
delay(2000L)
}
上面的代码在launch1、launch2、launch3中都有一个错误,使用SupervisorJob()
时再日志中就会输出三个错误信息/ by zero
,如果使用try-catch
就需要在每一个launch的错误处添加try-catch
,就像下面这样:
fun catchTest() = runBlocking {
val scope = CoroutineScope(SupervisorJob())
scope.launch {
launch {
println("satrt launch1")
delay(1000L)
try {
1 / 0 //刻意制造一个错误
} catch (e: ArithmeticException) {
println("launch1: $e")
}
println("end launch1")
}
launch {
println("satrt launch2")
delay(1000L)
try {
1 / 0 //刻意制造一个错误
} catch (e: ArithmeticException) {
println("launch2: $e")
}
println("end launch2")
}
launch {
println("satrt launch3")
delay(1000L)
try {
1 / 0 //刻意制造一个错误
} catch (e: ArithmeticException) {
println("launch3: $e")
}
println("end launch3")
}
}
delay(2000L)
}
//输出结果:
//satrt launch1
//satrt launch2
//satrt launch3
//launch1: java.lang.ArithmeticException: / by zero
//end launch1
//launch2: java.lang.ArithmeticException: / by zero
//launch3: java.lang.ArithmeticException: / by zero
//end launch3
//end launch2
问题解决但是太啰嗦,而且前面也聊过try-catch
放在全局会失效。
那么用CoroutineExceptionHandler的处理方式如下:
fun catchTest() = runBlocking {
val exception = CoroutineExceptionHandler { _, throwable ->
println("catch: $throwable")
}
val scope = CoroutineScope(exception)
scope.launch {
launch {
println("satrt launch1")
delay(1000L)
1 / 0 //刻意制造一个错误
println("end launch1")
}
launch {
println("satrt launch2")
delay(1000L)
1 / 0 //刻意制造一个错误
println("end launch2")
}
launch {
println("satrt launch3")
delay(1000L)
1 / 0 //刻意制造一个错误
println("end launch3")
}
}
delay(2000L)
}
//输出结果:
//satrt launch1
//satrt launch2
//satrt launch3
//catch: java.lang.ArithmeticException: / by zero
这样就可以发现CoroutineExceptionHandler的优势所在了。但是CoroutineExceptionHandler在使用时也会存在问题,比如说只把CoroutineExceptionHandler添加到报错的launch中,而不添加到父launch中,这样会不会有用呢?比如说下面这样:
fun catchTest() = runBlocking {
val exception = CoroutineExceptionHandler { _, throwable ->
println("catch: $throwable")
}
// 这里做了修改
val scope = CoroutineScope(coroutineContext)
scope.launch {
launch {
println("satrt launch1")
delay(1000L)
println("end launch1")
}
// exception移到了这里
launch(exception) {
println("satrt launch2")
delay(1000L)
1 / 0 //刻意制造一个错误
println("end launch2")
}
launch {
println("satrt launch3")
delay(1000L)
println("end launch3")
}
}
delay(2000L)
}
//输出结果:
//satrt launch1
//satrt launch2
//satrt launch3
Exception in thread "main" java.lang.ArithmeticException: / by zero
//end launch1
从日志就可以知道答案:不会,因为CoroutineExceptionHandler只在顶层的协程中才会起作用,这里引用CoroutineExceptionHandler注释中的一句话:所有的子协程(在另一个Job的上下文中创建的协程)将它们的异常处理委托给它们的父协程,父协程也委托给父协程,以此类推,直到根,所以安装在它们上下文中的CoroutineExceptionHandler永远不会被使用。与supervise job一起运行的协程不会将异常传播给它们的父协程,并被视为根协程。