kotlin协程-- 基础概念 ② |协程取消和异常处理

协程取消

取消协程的主要内容有:

  • 取消协程的作用域
  • 取消协程
    单独的协程取消,不会影响其余的兄弟协程
    • CPU密集型任务的取消
      • isActive
      • ensureAction
      • yield
  • 取消协程抛出的异常

取消协程的作用域

取消协程的作用域,会取消它的子协程

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 捕获整个作用域的所有异常。
相关推荐
闲暇部落23 分钟前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
长亭外的少年16 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
JIAY_WX16 小时前
kotlin
开发语言·kotlin
麦田里的守望者江1 天前
KMP 中的 expect 和 actual 声明
android·ios·kotlin
菠菠萝宝1 天前
【YOLOv8】安卓端部署-1-项目介绍
android·java·c++·yolo·目标检测·目标跟踪·kotlin
恋猫de小郭1 天前
Kotlin Multiplatform 未来将采用基于 JetBrains Fleet 定制的独立 IDE
开发语言·ide·kotlin
枫__________2 天前
kotlin 协程 job的cancel与cancelAndJoin区别
android·开发语言·kotlin
鸠摩智首席音效师2 天前
如何在 Ubuntu 上配置 Kotlin 应用环境 ?
linux·ubuntu·kotlin
jikuaidi6yuan4 天前
Java与Kotlin在鸿蒙中的地位
java·kotlin·harmonyos
liulanba4 天前
Kotlin的data class
前端·微信·kotlin