协程取消
取消协程的主要内容有:
- 取消协程的作用域
- 取消协程
单独的协程取消,不会影响其余的兄弟协程- CPU密集型任务的取消
- isActive
- ensureAction
- yield
- CPU密集型任务的取消
- 取消协程抛出的异常
取消协程的作用域
取消协程的作用域,会取消它的子协程
Kotlin
fun coroutineScopeCancel() {
//等待子协程执行完
runBlocking<Unit> {
//CoroutineScope不会继承runBlocking的属性。需要delay或者join
val scope = CoroutineScope(Dispatchers.Default)
scope.launch {
delay(1000)
Log.d("liu","启动 job 1")
}
scope.launch {
delay(1000)
Log.d("liu","启动 job 2")
}
//需要挂起,等待scope执行完
delay(2000)
}
}
不加delay的话,scope不会执行
Kotlin
fun coroutineScopeCancel() {
//等待子协程执行完
runBlocking<Unit> {
//CoroutineScope不会继承runBlocking的属性。需要delay或者join
val scope = CoroutineScope(Dispatchers.Default)
scope.launch {
delay(1000)
Log.d("liu","启动 job 1")
}
scope.launch {
delay(1000)
Log.d("liu","启动 job 2")
}
//需要挂起,等待scope执行完
delay(200)
scope.cancel()
delay(2000)
}
}
这样什么也不会打印,可以看出通过scope已经取消了其所有子协程 。
取消协程
协程的任务被取消后,不会影响兄弟协程的任务。
Kotlin
fun coroutineCancel1() {
runBlocking {
val job1 = launch {
delay(1000)
Log.d("liu","启动 job 1")
}
val job2 = launch {
delay(1000)
Log.d("liu","启动 job 2")
}
job1.cancel()
}
}
...
//打印结果
启动 job 2
这里可以看出。job1被取消后,job2还是会执行**。**
CPU密集型(计算)任务的取消
有2种方法可以取消计算代码的任务。
第一种方法是周期性地调用检查取消的挂起函数(借助Job生命周期,通过ensureActive()函数)。
另外一种就是通过yield()函数,明确检查Job是否处于取消状态。
借助Job生命周期处理
CPU密集型任务(计算量大),通过cancel是无法及时取消的。这个时候,我们可以通过Job的生命周期的辅助来帮助我们取消计算。
案例
我们每个一秒,打印一次Log日志。然后,中途取消。
看下代码
Kotlin
fun coroutineCancelCPUIntensiveTask1() {
runBlocking {
val startTime = System.currentTimeMillis()
val job = launch {
var nextTime = startTime
var totalPrintCount = 0
while (totalPrintCount < 5) {
if (System.currentTimeMillis() >= nextTime) {
nextTime = 1000
totalPrintCount++
Log.d("liu", "+++日志打印: $totalPrintCount")
}
}
}
delay(1000)
Log.d("liu", "开始执行取消")
job.cancel()
Log.d("liu", "执行取消完成")
}
}
通过执行结果,可以看到,执行取消后,计算仍然是完成了之后,才会取消
这个时候,我们通过Job的生命周期知道,在执行cancel后,Job的状态State就变成了Cancelling。
而Cancelling对应的我们可访问的Job属性isActive就变成了false。
这样,我们在while循环中,通过添加isActive是正在计算(是否为true),来辅助我们进行计算,以便在取消(Cancel)后,可以及时退出。
示例:
Kotlin
fun coroutineCancelCPUIntensiveTask2() {
runBlocking {
val startTime = System.currentTimeMillis()
val job = launch {
var nextTime = startTime
var totalPrintCount = 0
while (totalPrintCount < 5 && isActive) {
if (System.currentTimeMillis() >= nextTime) {
nextTime = 1000
totalPrintCount++
Log.d("liu", "+++日志打印: $totalPrintCount")
}
}
}
delay(1000)
Log.d("liu", "开始执行取消")
job.cancel()
Log.d("liu", "执行取消完成")
}
}
通过ensureActive()来处理
在协程执行过程中,我们也可以通过ensureActive()函数,来确认协程是否是活跃状态。如果是非活跃状态的话,它会抛出协程特有的异常来取消协程。
Kotlin
fun coroutineCancelCPUIntensiveTask3() {
runBlocking {
val startTime = System.currentTimeMillis()
val job = launch {
var nextTime = startTime
var totalPrintCount = 0
while (totalPrintCount < 5 ) {
ensureActive()
if (System.currentTimeMillis() >= nextTime) {
nextTime = 1000
totalPrintCount++
Log.d("liu", "+++日志打印: $totalPrintCount")
}
}
}
delay(1000)
Log.d("liu", "开始执行取消")
job.cancel()
Log.d("liu", "执行取消完成")
}
}
这段代码也是可以取消的。跟上面代码类似。取消isActive判断,添加ensureActive()判断。
其实ensureActive()函数,内部也是通过抛出异常来处理的,只不过是被静默处理了。后面会详细说明协程的异常处理。
ensureActive()函数的代码
public fun CoroutineScope.ensureActive(): Unit = coroutineContext.ensureActive()
调用协程上下文的ensureActive()函数
Kotlin
public fun CoroutineContext.ensureActive() {
get(Job)?.ensureActive()
}
调用了Job的ensureActive()函数
Kotlin
public fun Job.ensureActive(): Unit {
if (!isActive) throw getCancellationException()
}
getCancellationException()。估计就是获取,我们Cancel()时的可选异常
Kotlin
public fun getCancellationException(): CancellationException
这里可以看到,如果不是isActive活跃状态的话,就抛出了异常CancellationException。
CancellationException这个异常在我们执行Cancel()的时候,是可选参数。
到这里,我们就知道了,当我们想取消而无法取消协程时,我们也是可以通过,主动抛出这个异常来取消的,因为这个异常,协程会静默处理掉(上面普通协程取消时候,已经分析过)。
通过yield()函数来处理
yield()函数会检查协程状态(也是通过判断job是否是活跃状态),如果是非活跃状态,也会抛出异常,取消协程。
此外,它还会尝试出让线程的执行权,给其它协程提供执行机会。
首先,我们先看下yield()函数
Kotlin
public suspend fun yield(): Unit = suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
val context = uCont.context
context.checkCompletion()
....
}
接着看
Kotlin
internal fun CoroutineContext.checkCompletion() {
val job = get(Job)
if (job != null && !job.isActive) throw job.getCancellationException()
}
里,看到如果job不存在或者处于非活跃状态(!isActive)的话,就抛出了job.getCancellationException()异常。这个函数,我们在看ensureActive()时,刚看了,就是CancellationException异常。
所以,yield()函数也会检查协程是否处于活跃状态,不是的话,直接抛异常取消。
Kotlin
fun coroutineCancelCPUIntensiveTaskYield() {
runBlocking {
val startTime = System.currentTimeMillis()
val job = launch {
var nextTime = startTime
var totalPrintCount = 0
while (totalPrintCount < 5 ) {
yield()
if (System.currentTimeMillis() >= nextTime) {
nextTime = 1000
totalPrintCount++
Log.d("liu", "+++日志打印: $totalPrintCount")
}
}
}
delay(1000)
Log.d("liu", "开始执行取消")
job.cancel()
Log.d("liu", "执行取消完成")
}
}
在计算量特别大的时候,尽量使用yield()函数
取消协程抛出的异常
取消协程是通过抛出异常(CancellationException)来取消的。
在cancel的时候,是可以传入我们定义的异常的。但是,没有传入的话,为什么也没有发现异常?
这个是Kotlin内部已经自己处理了。
我们看下Job的cancel()方法
Kotlin
public interface Job : CoroutineContext.Element {
...
public fun cancel(cause: CancellationException? = null)
...
}
可以看出,当我们cancel的时候,是一个可选型的函数的
我们通过try捕获一下异常
Kotlin
fun coroutineCancelException() {
runBlocking {
runBlocking {
val job1 = launch {
try {
delay(1000)
Log.d("liu", "启动 job 1")
} catch (e: CancellationException) {
e.printStackTrace()
}
}
val job2 = launch {
delay(1000)
Log.d("liu", "启动 job 2")
}
job1.cancel()
}
}
}
我们再自定义一个异常看看
Kotlin
fun coroutineCancelException() {
runBlocking {
runBlocking {
val job1 = launch {
delay(1000)
Log.d("liu","启动 job 1")
}
val job2 = launch {
delay(1000)
Log.d("liu","启动 job 2")
}
job1.cancel(CancellationException("主动抛出异常"))
}
}
}
打印结果:
主动抛出异常
取消协程后,资源释放
捕获异常释放资源
上面取消协程时,我们讲了,取消协程时通过抛出异常来实现的。
我们可以使用try...catch来捕获这个异常。那么,如果,我们有需要释放的资源,也可以通过try...catch...finally,在finally中来释放我们的资源.
Kotlin
fun coroutineCancelResourceRelease() {
runBlocking {
runBlocking {
val job1 = launch {
try {
delay(1000)
Log.d("liu", "启动 job 1")
}finally {
Log.d("liu", "finally job 1")
}
}
val job2 = launch {
delay(1000)
Log.d("liu", "启动 job 2")
}
job1.cancel()
}
}
}
这里,可以看到finally中的代码执行了。如果,我们想释放资源的话,我们可以try挂起点,在finally中释放资源.
通过use()函数,释放资源
该函数只能被实现了Closeable的对象使用,程序结束的时候会自动调用close()方法,适合文件对象不是use()函数。
Kotlin
fun coroutineCancelResourceReleaseByUse() {
runBlocking {
val buffer = BufferedReader(FileReader("xxx"))
with(buffer) {
var line: String?
try {
while (true) {
line = readLine() ?: break;
Log.d("liu", line)
}
} finally {
close()
}
}
}
}
需要自己在finally中关闭资源。
使用use()函数
Kotlin
fun coroutineCancelResourceReleaseByUse() {
runBlocking {
BufferedReader(FileReader("xxx")).use {
while (true) {
val line = readLine() ?: break;
Log.d("liu", line)
}
}
}
}
不需要我们自己关闭。看下use函数
Kotlin
public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R {
var exception: Throwable? = null
try {
return block(this)
...
} finally {
...
try {
close()
} catch (closeException: Throwable) {
// cause.addSuppressed(closeException) // ignored here
}
....
}
}
该函数已经在finally中实现了,该功能。
NonCancellable-取消中的挂起函数
一个NonCancellable的Job总是处于活跃状态。它是为withContext()函数设计的。以防止取消时,需要在不取消的情况下执行的代码块。
比如,协程任务执行的失败。调用接口通知后台等情况。
NonCancellable该对象不适用于launch、async和其它的协程构建者。如果,你再launch中使用的话,那么当父协程取消的时候,不仅新启动的子协程不会被取消,父级和子级的关系也会被切断。父级不会等着子级执行完成,也不会在子级异常后取消。
Kotlin
withContext(NonCancellable) {
// 这里执行的代码块,不会被取消
}
一般情况下,在取消协程时,如果,我们通过try...finally捕获后,在finally中释放资源。
如果,finally中有挂起函数的话,那么该函数是不能执行的(协程处于非活跃状态)
处于取消中状态的协程不能使用挂起函数,当协程被取消后需要调用挂起函数的话,就需要通过NonCancellable来让挂起函数处于活跃状态,这样会挂起运行中的代码。
Kotlin
fun coroutineUnCancelException() {
runBlocking {
runBlocking {
val job1 = launch {
try {
repeat(1000) { i ->
delay(1000)
Log.d("liu", "启动 job 1,index: $i")
}
} catch (e: CancellationException) {
e.printStackTrace()
} finally {
delay(2000)
Log.d("liu", "finally job 1")
}
}
job1.cancel()
}
}
}
在取消时,finally中也有挂起函数(就想任务成功/失败,都通知后台一样)
这个时候,finally里面的日志是打印不出来的
我们需要用到NonCancellable
Kotlin
fun coroutineUnCancelException2() {
runBlocking {
runBlocking {
val job1 = launch {
try {
repeat(1000) { i ->
delay(1000)
Log.d("liu", "启动 job 1,index: $i")
}
} finally {
//在cancel里,有挂起函数后,需要用到NonCancellable
withContext(NonCancellable){
delay(2000)
Log.d("liu", "finally job 1")
}
}
}
job1.cancel()
}
}
}
这样的话,finally中的日志就能成功打印了。如果,我们需要在出现异常时候,调用网络请求等挂起函数的话,可以通过这种方式来完成。
超时任务的取消(withTimeout)
取消协程执行的最实际的原因就是它执行时间超时,比如,网络请求等。
虽然,我们可以有其它的方式追踪取消这样的任务,但是,我们可以直接使用withTimeout这个函数,达到同样的效果
Kotlin
withTimeout(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
打印结果
Kotlin
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms
之前,我们取消协程也会抛出异常,都会是被静默处理的。
这里,我们可以看到,超时任务会抛出TimeoutCancellationException异常,是没有被静默处理的。
但是,有时候,我们不希望抛出异常,我们希望返回Null或者一个默认值。那么,我们就需要用到withTimeoutOrNull。
示例代码:
Kotlin
fun coroutineTimeOutOrNull() {
runBlocking {
launch {
val result = withTimeoutOrNull(1300L) {
repeat(1000) { i ->
Log.d("liu", "I'm sleeping $i ...")
delay(500L)
}
"执行完成"
} ?: "执行未完成,默认值"
Log.d("liu","Result is $result")
}
}
}
这里,我们看到,返回的就是一个默认值。
协程异常处理
协程是互相协作的程序,协程是结构化的。
正是因为协程的这两个特点,导致它和 Java 的异常处理机制不一样。如果将 Java 的异常处理机制照搬到Kotlin协程中,会遇到很多问题,如:协程无法取消、try-catch不起作用等。
Kotlin协程中的异常主要分两大类
- 协程取消异常(CancellationException)
- 其他异常
异常处理六大准则
- 协程的取消需要内部配合。
- 不要打破协程的父子结构。
- 捕获 CancellationException 异常后,需要考虑是否重新抛出来。
- 不要用 try-catch 直接包裹 launch、async。
- 使用 SurpervisorJob 控制异常传播的范围。
- 使用 CoroutineExceptionHandler 处理复杂结构的协程异常,仅在顶层协程中起作用。
核心理念:协程是结构化的,异常传播也是结构化的。
准则一:协程的取消需要内部配合
协程任务被取消时,它的内部会产生一个 CancellationException 异常,协程的结构化并发的特点:如果取消了父协程,则子协程也会跟着取消。
问题:cancel不被响应
Kotlin
fun main() = runBlocking {
val job = launch(Dispatchers.Default) {
var i = 0
while (true) {
Thread.sleep(500L)
i++
println("i: $i")
}
}
delay(200L)
job.cancel()
job.join()
println("End")
}
/*
输出信息:
i: 1
i: 2
i: 3
i: 4
// 不会停止,一直打印输出
*/
原因:协程是相互协作的程序,因此协程任务的取消也需要相互协作。协程外部取消,协程内部需要做出相应。
解决:使用 isActive 判断是否处于活跃状态
Kotlin
fun main() = runBlocking {
val job = launch(Dispatchers.Default) {
var i = 0
// 关键
// ↓
while (isActive) {
Thread.sleep(500L)
i++
println("i: $i")
}
}
delay(200L)
job.cancel()
job.join()
println("End")
}
/*
输出信息:
i: 1
End
*/
准则二:不要打破协程的父子结构
问题:子协程不会跟随父协程一起取消
Kotlin
val fixedDispatcher = Executors.newFixedThreadPool(2) {
Thread(it, "MyFixedThread")
}.asCoroutineDispatcher()
fun main() = runBlocking {
// 父协程
val parentJob = launch(fixedDispatcher) {
//子协程1
launch(Job()) {
var i = 0
while (isActive) {
Thread.sleep(500L)
i++
println("子协程1 i:$i")
}
}
//子协程2
launch {
var i = 0
while (isActive) {
Thread.sleep(500L)
i++
println("子协程2 i:$i")
}
}
}
delay(1000L)
parentJob.cancel()
parentJob.join()
println("End")
}
/*
输出信息:
子协程1 i:1
子协程2 i:1
子协程2 i:2
子协程1 i:2
End
子协程1 i:3
子协程1 i:4
子协程1 i:5
// 子协程1一直在执行,不会停下来
*/
原因:协程是结构化的,取消啦父协程,子协程也会被取消。但是在这里"子协程1"不在 parentJob 的子协程,打破了原有的结构化关系,当调用 parentJob.cancel 时,"子协程1"就不会被取消了。
解决:不破坏父子结构
"子协程1"不要传入额外的 Job()。
Kotlin
fun main() = runBlocking {
val parentJob = launch(fixedDispatcher) {
launch {
var i = 0
while (isActive) {
Thread.sleep(500L)
i++
println("子协程1:i= $i")
}
}
launch {
var i = 0
while (isActive) {
Thread.sleep(500L)
i++
println("子协程2:i= $i")
}
}
}
delay(2000L)
parentJob.cancel()
parentJob.join()
println("end")
}
/*
输出结果:
子协程1:i= 1
子协程2:i= 1
子协程2:i= 2
子协程1:i= 2
子协程1:i= 3
子协程2:i= 3
子协程1:i= 4
子协程2:i= 4
end
*/
准则三:捕获 CancellationException 需要重新抛出来
挂起函数可以自动响应协程的取消
Kotlin 中的挂起函数是可以自动响应协程的取消,如下中的 delay() 函数可以自动检测当前协程是否被取消,如果已经取消了它就会抛出一个 CancellationException,从而终止当前协程。
Kotlin
fun main() = runBlocking {
// 父协程
val parentJob = launch(Dispatchers.Default) {
//子协程1
launch {
var i = 0
while (true) {
// 这里
delay(500L)
i++
println("子协程1 i:$i")
}
}
//子协程2
launch {
var i = 0
while (true) {
// 这里
delay(500L)
i++
println("子协程2 i:$i")
}
}
}
delay(1000L)
parentJob.cancel()
parentJob.join()
println("End")
}
/*
输出信息:
子协程1 i:1
子协程2 i:1
子协程1 i:2
子协程2 i:2
End
*/
Kotlin
fun main() = runBlocking {
// 父协程
val parentJob = launch(Dispatchers.Default) {
//子协程1
launch {
var i = 0
while (true) {
try {
delay(500L)
} catch (e: CancellationException) {
println("捕获CancellationException")
throw e
}
i++
println("子协程1 i:$i")
}
}
//子协程2
launch {
var i = 0
while (true) {
try {
delay(500L)
} catch (e: CancellationException) {
println("捕获CancellationException")
throw e
}
i++
println("子协程2 i:$i")
}
}
}
delay(1000L)
parentJob.cancel()
parentJob.join()
println("End")
}
/*
输出信息:
子协程1 i:1
子协程2 i:1
捕获CancellationException
捕获CancellationException
End
*/
协程 1, 2 都 抓取异常,抛出一个 CancellationException。注意:这里其实有问题,看下一个。
问题:捕获 CancellationException 导致崩溃
Kotlin
fun main() = runBlocking {
val parentJob = launch(Dispatchers.Default) {
launch {
var i = 0
while (true) {
try {
delay(500L)
} catch (e: CancellationException) {
println("捕获CancellationException异常")
}
i++
println("子协程1 i= $i")
}
}
launch {
var i = 0
while (true) {
delay(500L)
i++
println("子协程2 i= $i")
}
}
}
delay(2000L)
parentJob.cancel()
parentJob.join()
println("end")
}
/*
输出信息:
子协程1 i= 1
子协程2 i= 1
子协程1 i= 2
子协程2 i= 2
子协程1 i= 3
子协程2 i= 3
捕获CancellationException异常
...... //程序不会终止
*/
原因:当捕获到 CancellationException 以后,还需要将它重新抛出去,如果没有抛出去则子协程将无法取消。
解决:需要重新抛出
以上三条准则,都是应对 CancellationException 这个特殊异常的。
最终解决:
Kotlin
fun main() = runBlocking {
val parentJob = launch(Dispatchers.Default) {
launch {
var i = 0
while (true) {
try {
delay(500L)
} catch (e: CancellationException) {
println("捕获CancellationException异常")
// 抛出异常
throw e
}
i++
println("协程1 i= $i")
}
}
launch {
var i = 0
while (true) {
delay(500L)
i++
println("协程2 i= $i")
}
}
}
delay(2000L)
parentJob.cancel()
parentJob.join()
println("end")
}
/*
输出信息:
协程1 i= 1
协程2 i= 1
协程2 i= 2
协程1 i= 2
协程2 i= 3
协程1 i= 3
捕获CancellationException异常
end
*/
第一个协程 catch CancellationException 异常,然后在里面throw出去。
准则四:不要用try-catch直接包裹launch、async
问题:try-catch不起作用
Kotlin
fun main() = runBlocking {
try {
launch {
delay(100L)
1 / 0 //产生异常
}
} catch (e: ArithmeticException) {
println("捕获:$e")
}
delay(500L)
println("end")
}
/*
输出信息:
Exception in thread "main" java.lang.ArithmeticException: / by zero
*/
原因:协程的代码执行顺序与普通程序不一样,当协程执行 1 / 0
时,程序实际已经跳出 try-catch 的作用域了,所以直接使用 try-catch 包裹 launch、async 是没有任何效果的。
解决:调整作用域
可以将 try-catch 移动到协程体内部,这样可以捕获到异常了。
Kotlin
fun main() = runBlocking {
launch {
delay(100L)
try {
1 / 0 //产生异常
} catch (e: ArithmeticException) {
println("捕获异常:$e")
}
}
delay(500L)
println("end")
}
/*
输出信息:
捕获异常:java.lang.ArithmeticException: / by zero
end
*/
准则五:灵活使用SurpervisorJob
问题:子Job发生异常影响其他子Job
Kotlin
fun main() = runBlocking {
launch {
launch {
1 / 0
delay(100L)
println("hello world 111")
}
launch {
delay(200L)
println("hello world 222")
}
launch {
delay(300L)
println("hello world 333")
}
}
delay(1000L)
println("end")
}
/*
输出信息:
Exception in thread "main" java.lang.ArithmeticException: / by zero
*/
原因:使用普通 Job 时,当子Job发生异常时,会导致 parentJob 取消,从而导致其他子Job也受到牵连,这也是协程结构化的体现。
1、解决:使用 SupervisorJob
SurpervisorJob 是 Job 的子类,SurpervisorJob 是一个种特殊的 Job ,可以控制异常的传播范围,当子Job发生异常时,其他的子Job不会受到影响。
将 parentJob 改为 SupervisorJob。
Kotlin
fun main() = runBlocking {
val scope = CoroutineScope(SupervisorJob())
scope.launch {
1 / 0
delay(100L)
println("hello world 111")
}
scope.launch {
delay(200L)
println("hello world 222")
}
scope.launch {
delay(300L)
println("hello world 333")
}
delay(1000L)
println("end")
}
/*
输出信息:
Exception in thread "DefaultDispatcher-worker-1 @coroutine#2" java.lang.ArithmeticException: / by zero
hello world 222
hello world 333
end
*/
2、解决:使用 supervisorScope
supervisorScope 底层依然使用的是 SupervisorJob。
Kotlin
fun main() = runBlocking {
supervisorScope {
launch {
1 / 0
delay(100L)
println("hello world 111")
}
launch {
delay(200L)
println("hello world 222")
}
launch {
delay(300L)
println("hello world 333")
}
}
delay(1000L)
println("end")
}
/*
输出信息:
Exception in thread "main" java.lang.ArithmeticException: / by zero
hello world 222
hello world 333
end
*/
准则六:使用 CoroutineExceptionHandler 处理复杂结构的协程异常
问题:复杂结构的协程异常
Kotlin
fun main() = runBlocking {
val scope = CoroutineScope(coroutineContext)
scope.launch {
async { delay(100L) }
launch {
delay(100L)
launch {
delay(100L)
1 / 0
}
}
delay(100L)
}
delay(1000L)
println("end")
}
/*
输出信息:
Exception in thread "main" java.lang.ArithmeticException: / by zero
*/
原因:模拟一个复杂的协程嵌套场景,开发人员很难在每一个协程体中写 try-catch,为了捕获异常,可以使用 CoroutineExceptionHandler。
解决:使用CoroutineExceptionHandler
用 CoroutineExceptionHandler 处理复杂结构的协程异常,它只能在顶层协程中起作用。
Kotlin
fun main() = runBlocking {
val myCoroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
println("捕获异常:$throwable")
}
val scope = CoroutineScope(coroutineContext + Job() + myCoroutineExceptionHandler)
scope.launch {
async { delay(100L) }
launch {
delay(100L)
launch {
delay(100L)
1 / 0
}
}
delay(100L)
}
delay(1000L)
println("end")
}
/*
输出信息:
捕获异常:java.lang.ArithmeticException: / by zero
end
*/
总结
- 准则一:协程的取消需要内部的配合。
- 准则二:不要轻易打破协程的父子结构。协程的优势在于结构化并发,他的许多特性都是建立在这之上的,如果打破了它的父子结构,会导致协程无法按照预期执行。
- 准则三:捕获 CancellationException 异常后,需要考虑是否重新抛出来。协程是依赖 CancellationException 异常来实现结构化取消的,捕获异常后需要考虑是否重新抛出来。
- 准则四:不要用 try-catch 直接包裹 launch、async。协程代码的执行顺序与普通程序不一样,直接使用 try-catch 可能不会达到预期效果。
- 准则五:使用 SupervisorJob 控制异常传播范围。SupervisorJob 是一种特殊的 Job,可以控制异常的传播范围,不会受到子协程中的异常而取消自己。
- 准则六:使用 CoroutineExceptionHandler 捕获异常。当协程嵌套层级比较深时,可以在顶层协程中定义 CoroutineExceptionHandler 捕获整个作用域的所有异常。