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 捕获整个作用域的所有异常。
相关推荐
小书房4 小时前
Kotlin的内联函数
java·开发语言·kotlin·inline·内联函数
zhangphil7 小时前
Android Page3与Flow分页查媒体数据库展示宫格图片列表,Kotlin
android·kotlin
胡致和1 天前
配置变更后,弹窗为什么飞到了最左边?
kotlin
zhangphil1 天前
Android Page 3 Flow读sql数据库媒体文件,Kotlin
android·kotlin
小书房1 天前
Kotlin使用体验及理解1
android·开发语言·kotlin
Kapaseker1 天前
我想让同事知道我很懂 Compose 怎么办?
android·kotlin
jinanwuhuaguo2 天前
OpenClaw工程解剖——RAG、向量织构与“记忆宫殿”的索引拓扑学(第十三篇)
android·开发语言·人工智能·kotlin·拓扑学·openclaw
jinanwuhuaguo2 天前
OpenClaw协议霸权——从 MCP 标准到意图封建化的政治经济学(第十八篇)
android·人工智能·kotlin·拓扑学·openclaw
zhangphil2 天前
Android sql查媒体数据封装room Dao构造AndroidViewModel,RecyclerView宫格展示,Kotlin
android·kotlin
jinanwuhuaguo2 天前
反熵共同体——OpenClaw的宇宙热力学本体论(第十七篇)
大数据·人工智能·安全·架构·kotlin·openclaw