在 Kotlin 协程当中,我们通常把异常分为两大类,一类是取消异常(CancellationException),另一类是其他异常。在 Kotlin 协程当中,这两种异常的处理方式是不一样的。
当协程任务被取消的时候,它的内部是会产生一个 CancellationException 的。而协程的结构化并发,最大的优势就在于:如果我们取消了父协程,子协程也会跟着被取消。
- val job = launch(Dispatchers.Default) {
- var i = 0
- while (true) {
- Thread.sleep(500L)
- i++
- println("i =$i")
- }
- }
-
- delay(2000L)
- job.cancel()
- job.join()
- println("END")
- }
- Log
-
- i =1
- i =2
- i =3
- i =4
- i =5
- i =6
- i =7
- i =8
- i =9
- i =10
- i =11
- i =12
- ......
- 程序无法停止
上面的程序无法停止,协程任务的取消,需要互相协作。协程外部取消,协程内部需要做出响应才行。当我们调用 job.cancel() 以后,协程任务已经不是活跃状态了,但代码并没有把 isActive 作为循环条件,因此协程无法真正取消。
可以在协程体中加入状态判断:
- runBlocking {
- val job = launch(Dispatchers.Default) {
- var i = 0
- while (isActive) {
- Thread.sleep(500L)
- i++
- println("i =$i")
- }
- }
-
- delay(2000L)
- job.cancel()
- job.join()
- println("END")
- }
-
- Log
-
- i =1
- i =2
- i =3
- i =4
- END
-
- Process finished with exit code 0
把 while 循环的条件改成了 while (isActive),这就意味着,只有协程处于活跃状态的时候,才会继续执行循环体内部的代码。协程的取消需要内部的配合。
协程是结构化的,当我们取消父协程的时候,子协程也会跟着被取消。
但是特殊情况是嵌套创建的子协程并不会跟随父协程一起取消。
- runBlocking {
- val parentJob = launch(fixedDispatcher) {
- launch(Job()) {
- var i = 0
- while (isActive) {
- Thread.sleep(500L)
- i++
- println("First i:$i")
- }
- }
-
- launch {
- var i = 0
- while (isActive) {
- Thread.sleep(500L)
- i++
- println("Second i:$i")
- }
- }
- }
-
- delay(2000L)
- parentJob.cancel()
- parentJob.join()
-
- println("End")
- }
-
- Log
-
- Second i:1
- First i:1
- First i:2
- Second i:2
- Second i:3
- First i:3
- Second i:4
- First i:4
- End
- First i:5
- First i:6
- First i:7
- First i:8
- First i:9
- First i:10
- First i:11
- First i:12
- First i:13
- First i:14
- First i:15
- ......
可以发现,创建子协程的时候,使用了 launch(Job()){},就打破了原有的协程结构。因为 launch(Job()){}创建的协程的父 Job 是在 launch 当中传入的 Job() 对象。所以调用 parentJob.cancel() 的时候,无法销毁该协程。
可以按如下修改:
- runBlocking {
- val parentJob = launch(fixedDispatcher) {
- launch {
- var i = 0
- while (isActive) {
- Thread.sleep(500L)
- i++
- println("First i:$i")
- }
- }
-
- launch {
- var i = 0
- while (isActive) {
- Thread.sleep(500L)
- i++
- println("Second i:$i")
- }
- }
- }
-
- delay(2000L)
- parentJob.cancel()
- parentJob.join()
-
- println("End")
- }
-
-
- First i:1
- Second i:1
- First i:2
- Second i:2
- First i:3
- Second i:3
- First i:4
- Second i:4
- End
parentJob 与它内部的子协程之间都是父子关系,因此它们两个都是会响应协程取消的事件的。不要轻易打破协程的父子结构!
对于 Kotlin 提供的挂起函数,可以自动响应协程的取消。
例如:
- runBlocking {
- val parentJob = launch(Dispatchers.Default) {
- launch {
- var i = 0
- while (true) {
- delay(500L)
- i++
- println("First i = $i")
- }
- }
-
- launch {
- var i = 0
- while (true) {
- delay(500L)
- i++
- println("Second i = $i")
- }
- }
- }
- delay(2000L)
- parentJob.cancel()
- parentJob.join()
-
- println("End")
- }
-
- First i = 1
- Second i = 1
- First i = 2
- Second i = 2
- First i = 3
- Second i = 3
- End
-
- Process finished with exit code 0
delay() 函数可以自动检测当前的协程是否已经被取消,如果已经被取消的话,它会抛出一个 CancellationException,从而终止当前的协程。
- runBlocking {
- val parentJob = launch(Dispatchers.Default) {
- launch {
- var i = 0
- while (true) {
- try {
- delay(500L)
- } catch (e: CancellationException) {
- println("Catch CancellationException")
- throw e
- }
- i++
- println("First i =$i")
- }
- }
-
- launch {
- var i = 0
- while (true) {
- delay(500L)
- i++
- println("Second i = $i")
- }
- }
- }
-
- delay(2000L)
- parentJob.cancel()
- parentJob.join()
-
- println("END")
- }
-
- Log:
-
- First i =1
- Second i = 1
- First i =2
- Second i = 2
- First i =3
- Second i = 3
- Catch CancellationException
- END
-
- Process finished with exit code 0
try-catch 包裹了 delay() 以后,打印出“Catch CancellationException”,这就说明 delay() 确实可以自动响应协程的取消,并且产生 CancellationException 异常。
注意:捕获了 CancellationException 以后没有重新抛出去,就导致子协程无法正常取消。
- runBlocking {
- val parentJob = launch(Dispatchers.Default) {
- launch {
- var i = 0
- while (true) {
- try {
- delay(500L)
- } catch (e: CancellationException) {
- println("Catch CancellationException")
- //throw e
- }
- i++
- println("First i =$i")
- }
- }
-
- launch {
- var i = 0
- while (true) {
- delay(500L)
- i++
- println("Second i = $i")
- }
- }
- }
-
- delay(2000L)
- parentJob.cancel()
- parentJob.join()
-
- println("END")
- }
- ......
- First i =656179
- Catch CancellationException
- First i =656180
- Catch CancellationException
- First i =656181
- Catch CancellationException
- First i =656182
- Catch CancellationException
- First i =656183
- Catch CancellationException
- .....
所以,捕获了 CancellationException 以后,要考虑是否应该重新抛出来。
- runBlocking {
- try {
- launch {
- delay(100L)
- 1 / 0
- }
- } catch (e: Exception) {
- println("catch: $e")
- }
- delay(500L)
- println("End")
- }
-
- Log
- Exception in thread "main" java.lang.ArithmeticException: / by zero
- at com.example.myapplication.testcoroutinue.TestTryCatchKt$testTryCatch8$1$1.invokeSuspend(TestTryCatch.kt:225)
- at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
- at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:234)
- at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:166)
- at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:397)
- at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:431)
- at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:420)
- at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:518)
- at kotlinx.coroutines.EventLoopImplBase$DelayedResumeTask.run(EventLoop.common.kt:494)
- at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
- at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
- at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
- at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
- at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
- at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
- at com.example.myapplication.testcoroutinue.TestTryCatchKt.testTryCatch8(TestTryCatch.kt:221)
- at com.example.myapplication.testcoroutinue.TestTryCatchKt.main(TestTryCatch.kt:15)
- at com.example.myapplication.testcoroutinue.TestTryCatchKt.main(TestTryCatch.kt)
-
- Process finished with exit code 1
可以发现:try-catch 并没有成功捕获异常,程序等待了 100 毫秒左右,最终还是崩溃了。
使用async
- runBlocking {
- var deffered: Deferred
? = null - try {
- deffered = async {
- delay(100L)
- 1 / 0
- }
- } catch (e: ArithmeticException) {
- println("Catch:$e")
- }
- deffered?.await()
- println("End")
- }
-
- Exception in thread "main" java.lang.ArithmeticException: / by zero
- at com.example.myapplication.testcoroutinue.TestTryCatchKt$testTryCatch9$1$1.invokeSuspend(TestTryCatch.kt:242)
- at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
- at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:234)
- at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:166)
- at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:397)
- at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:431)
- at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:420)
- at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:518)
- at kotlinx.coroutines.EventLoopImplBase$DelayedResumeTask.run(EventLoop.common.kt:494)
- at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
- at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
- at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
- at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
- at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
- at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
- at com.example.myapplication.testcoroutinue.TestTryCatchKt.testTryCatch9(TestTryCatch.kt:237)
- at com.example.myapplication.testcoroutinue.TestTryCatchKt.main(TestTryCatch.kt:16)
- at com.example.myapplication.testcoroutinue.TestTryCatchKt.main(TestTryCatch.kt)
-
- Process finished with exit code 1
当协程体当中的“1/0”执行的时候,程序已经跳出 try-catch 的作用域了,所以 try-catch失效。
把 try-catch 挪到 launch{} 协程体内部。可以正常捕获到 ArithmeticException 这个异常了。
- runBlocking {
- var deffered: Deferred
? = null - deffered = async {
- try {
-
- delay(100L)
- 1 / 0
-
- } catch (e: ArithmeticException) {
- println("Catch:$e")
- }
- }
- deffered.await()
- println("End")
- }
-
- Log
-
- Catch:java.lang.ArithmeticException: / by zero
- End
-
- Process finished with exit code 0
注意:不要用 try-catch 直接包裹 launch、async。
使用 try-catch 包裹“deferred.await()”。
例:
- runBlocking {
- var deffered = async {
- delay(100L)
- 1 / 0
- }
- try {
- deffered.await()
- } catch (e: Exception) {
- println("Catch:$e")
- }
- println("End")
- }
-
- atch:java.lang.ArithmeticException: / by zero
- End
- Exception in thread "main" java.lang.ArithmeticException: / by zero
- at com.example.myapplication.testcoroutinue.TestTryCatchKt$testTryCatch11$1$deffered$1.invokeSuspend(TestTryCatch.kt:275)
- at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
- at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:234)
- at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:166)
- at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:397)
- at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:431)
- at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:420)
- at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:518)
- at kotlinx.coroutines.EventLoopImplBase$DelayedResumeTask.run(EventLoop.common.kt:494)
- at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
- at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
- at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
- at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
- at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
- at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
- at com.example.myapplication.testcoroutinue.TestTryCatchKt.testTryCatch11(TestTryCatch.kt:272)
- at com.example.myapplication.testcoroutinue.TestTryCatchKt.main(TestTryCatch.kt:16)
- at com.example.myapplication.testcoroutinue.TestTryCatchKt.main(TestTryCatch.kt)
-
- Process finished with exit code 1
await() 如果不调用的话,async 当中的异常是否发生?
- runBlocking {
- var deffered = async {
- delay(100L)
- 1 / 0
- }
- delay(500L)
- println("End")
- }
-
- Exception in thread "main" java.lang.ArithmeticException: / by zero
- at com.example.myapplication.testcoroutinue.TestTryCatchKt$testTryCatch12$1$deffered$1.invokeSuspend(TestTryCatch.kt:290)
- at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
- at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:234)
- at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:166)
- at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:397)
- at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:431)
- at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:420)
- at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:518)
- at kotlinx.coroutines.EventLoopImplBase$DelayedResumeTask.run(EventLoop.common.kt:494)
- at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
- at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
- at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
- at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
- at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
- at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
- at com.example.myapplication.testcoroutinue.TestTryCatchKt.testTryCatch12(TestTryCatch.kt:287)
- at com.example.myapplication.testcoroutinue.TestTryCatchKt.main(TestTryCatch.kt:16)
- at com.example.myapplication.testcoroutinue.TestTryCatchKt.main(TestTryCatch.kt)
-
- Process finished with exit code 1
可见,async 当中产生异常,即使不调用 await() 同样是会导致程序崩溃的。
使用 try-catch 包裹“deferred.await()”,需要配合 SupervisorJob 一起使用。实现“不调用 await() 就不会产生异常而崩溃”。
- unBlocking {
- val scope = CoroutineScope(SupervisorJob())
- scope.async {
- delay(100L)
- 1 / 0
- }
- delay(500L)
- println("End")
- }
-
- Log
-
- End
-
- Process finished with exit code 0
使用 SupervisorJob 创建一个 scope 以后,用 scope.async{}启动协程后,只要不调用“deferred.await()”,程序就不会因为异常而崩溃。
- runBlocking {
- val coroutineScope = CoroutineScope(SupervisorJob())
- val deferred = coroutineScope.async {
- delay(100L)
- 1 / 0
- }
- try {
- deferred.await()
- } catch (e: Exception) {
- println("Catch:$e")
- }
- delay(500L)
- println("End")
- }
-
- Log
- Catch:java.lang.ArithmeticException: / by zero
- End
-
- Process finished with exit code 0
使用“coroutineScope.async {}”创建了协程,同时也用 try-catch 包裹“deferred.await()”,这样一来,异常就成功地被捕获了。
-
-
-
- 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 的子类。SupervisorJob 与 Job 最大的区别就在于,当它的子 Job 发生异常的时候,其他的子 Job 不会受到牵连。
对于普通 Job, 出现异常时的应对策略是:由于 parentJob 是一个普通的 Job 对象,当 job1 发生异常之后,它会导致 parentJob 取消,进而导致 job2、job3 也受到牵连。
如果把 parentJob 改为 SupervisorJob,job1 发生异常的的话,就不会影响到其他的 Job 了。
注意:灵活使用 SupervisorJob,控制异常传播的范围。
- runBlocking {
- val coroutineScope = CoroutineScope(coroutineContext)
- coroutineScope.launch {
- async {
- delay(100L)
- }
-
- launch {
- delay(100L)
- 1/0
- }
- }
- delay(1000L)
- println("END")
- }
-
-
- Exception in thread "main" java.lang.ArithmeticException: / by zero
- at com.example.myapplication.testcoroutinue.TestTryCatchKt$testTryCatch15$1$1$2.invokeSuspend(TestTryCatch.kt:338)
- at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
- at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:234)
- at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:166)
- at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:397)
- at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:431)
- at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:420)
- at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:518)
- at kotlinx.coroutines.EventLoopImplBase$DelayedResumeTask.run(EventLoop.common.kt:494)
- at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
- at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
- at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
- at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
- at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
- at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
- at com.example.myapplication.testcoroutinue.TestTryCatchKt.testTryCatch15(TestTryCatch.kt:329)
- at com.example.myapplication.testcoroutinue.TestTryCatchKt.main(TestTryCatch.kt:16)
- at com.example.myapplication.testcoroutinue.TestTryCatchKt.main(TestTryCatch.kt)
-
- Process finished with exit code 1
使用CoroutineExceptionHandler 处理上述代码中的异常。
- runBlocking {
- val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
- println("Catch: $throwable")
- }
- val coroutineScope = CoroutineScope(coroutineContext + Job() + coroutineExceptionHandler)
- coroutineScope.launch {
- async {
- delay(100L)
- }
-
- launch {
- delay(100L)
- 1 / 0
- }
- }
- delay(1000L)
- println("END")
- }
-
-
- Log
- Catch: java.lang.ArithmeticException: / by zero
- END
-
- Process finished with exit code 0
定义了一个 CoroutineExceptionHandler,然后把它传入了 scope 当中,就可以捕获其中所有的异常了。
注意点:在特定场景,为什么 CoroutineExceptionHandler 不起作用?
- runBlocking {
- val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
- println("Catch: $throwable")
- }
- val coroutineScope = CoroutineScope(coroutineContext)
- coroutineScope.launch {
- async {
- delay(100L)
- }
-
- launch(coroutineExceptionHandler) {
- delay(100L)
- 1 / 0
- }
- }
- delay(1000L)
- println("END")
- }
-
- Exception in thread "main" java.lang.ArithmeticException: / by zero
- at com.example.myapplication.testcoroutinue.TestTryCatchKt$testTryCatch17$1$1$2.invokeSuspend(TestTryCatch.kt:383)
- at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
- at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:234)
- at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:166)
- at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:397)
- at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:431)
- at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:420)
- at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:518)
- at kotlinx.coroutines.EventLoopImplBase$DelayedResumeTask.run(EventLoop.common.kt:494)
- at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
- at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
- at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
- at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
- at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
- at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
- at com.example.myapplication.testcoroutinue.TestTryCatchKt.testTryCatch17(TestTryCatch.kt:371)
- at com.example.myapplication.testcoroutinue.TestTryCatchKt.main(TestTryCatch.kt:16)
- at com.example.myapplication.testcoroutinue.TestTryCatchKt.main(TestTryCatch.kt)
-
- Process finished with exit code 1
把自定义的 myExceptionHandler,放到出现异常的 launch 那里传了进去。myExceptionHandler 并不会起作用,异常不会被它捕获。注意:myExceptionHandler 直接定义在发生异常的位置反而不生效,而定义在最顶层却可以生效。因为 只在顶层的协程当中才会起作用。也就是说,当子协程当中出现异常以后,它们都会统一上报给顶层的父协程,然后顶层的父协程才会去调用 CoroutineExceptionHandler,来处理对应的异常。所以需要记住:使用 CoroutineExceptionHandler 处理复杂结构的协程异常,它仅在顶层协程中起作用。