zl程序教程

您现在的位置是:首页 >  后端

当前栏目

【Kotlin 协程】协程异常处理 ⑤ ( 异常传播的特殊情况 | 取消子协程示例 | 子协程抛出异常后父协程处理异常时机示例 | 异常聚合 | 多个子协程抛出的异常会聚合到第一个异常中 )

Kotlin异常 处理 示例 多个 情况 第一个 取消
2023-09-14 09:07:26 时间





一、异常传播的特殊情况



【Kotlin 协程】协程异常处理 ① ( 根协程异常处理 | 自动传播异常 | 在协程体捕获异常 | 向用户暴露异常 | 在 await 处捕获异常 | 非根协程异常处理 | 异常传播特性 ) 博客中介绍到 协程 运行时 , 产生异常 , 会将异常 传递给 父协程 , 父协程会执行如下操作 :

  • ① 取消子协程 : 不仅仅取消产生异常的子协程 , 该父协程下所有的子协程都会取消 ;
  • ② 取消父协程 : 将父协程本身取消 ;
  • ③ 向父协程的父协程传播异常 : 继续将异常传播给 父协程的父协程 ;

但是也有特殊情况 :

  • 协程 调用 Job#cancel() 函数 进行取消操作时 , 会 抛出 CancellationException 异常 , 该异常是正常的操作 , 会被忽略 ;
  • 如果 抛出 CancellationException 异常 取消 子协程 , 其 父协程 不会受其影响 ;
  • 如果 子协程 抛出的是 其它异常 , 该异常会被传递给 父协程 进行处理 ;
  • 如果 父协程 有多个子协程 , 多个子协程 都抛出异常 , 父协程会等到 所有子协程 都执行完毕会后 , 再处理 异常 ;

1、取消子协程示例


在下面的代码中 , 在 父协程中 使用 launch 协程构建器 创建了子协程 , 注意 如果想要子协程运行 , 必须在创建完子协程后 调用 yield() 函数 , 让 父协程 让渡线程执行权 , 也就是令 子协程 执行起来 , 否则 主线程 一直占用线程 , 子协程无法执行 ;

子协程执行起来后 , 取消子协程 , 此时 在子协程中 , 会抛出 CancellationException 异常 , 该异常不会传递到 父协程 中 , 父协程 正常执行到结束 ;


代码示例 :

package kim.hsl.coroutine

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.yield

class MainActivity : AppCompatActivity() {
    val TAG = "MainActivity"
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        runBlocking {

            // 父协程
            val job = launch {
                Log.i(TAG, "父协程开始执行")

                // 子协程
                val childJob = launch {
                    Log.i(TAG, "子协程执行开始")
                    try {
                        delay(200)
                    }finally {
                        Log.i(TAG, "子协程执行 finally 代码")
                    }
                }

                // 让渡协程的执行权, 让子协程执行
                yield()

                Log.i(TAG, "取消子协程")
                childJob.cancel()

                Log.i(TAG, "父协程执行完毕")
            }

            // 等待父协程执行完毕
            job.join()
        }
    }
}

执行结果 :

23:38:43.639  I  父协程开始执行
23:38:43.640  I  子协程执行开始
23:38:43.642  I  取消子协程
23:38:43.643  I  父协程执行完毕
23:38:43.643  I  子协程执行 finally 代码

在这里插入图片描述


2、子协程抛出异常后父协程处理异常时机示例


父协程 中 使用 launch 创建了 2 个 子协程 ,

  • 子协程 1 执行 2 秒后 , 在 finally 中再执行 1 秒 ;
  • 子协程 2 执行 100 ms 后 , 自动抛出异常 ;

在 子协程 2 抛出异常后 , 两个子协程 都会退出 , 但是 子协程 1 的 finally 代码要执行 1000 ms , 这里父协程 等待 子协程 1 执行完毕后 , 才会处理 子协程 抛出的异常 ;


代码示例 :

package kim.hsl.coroutine

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.*

class MainActivity : AppCompatActivity() {
    val TAG = "MainActivity"
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        runBlocking {
            // 创建 协程异常处理器 CoroutineExceptionHandler
            val coroutineExceptionHandler = CoroutineExceptionHandler {
                    coroutineContext, throwable ->

                Log.i(TAG, "CoroutineExceptionHandler 中处理异常 " +
                        "\n协程上下文 ${coroutineContext}" +
                        "\n异常内容 ${throwable}")
            }

            // 父协程
            val job = GlobalScope.launch(coroutineExceptionHandler) {
                Log.i(TAG, "父协程开始执行")

                // 子协程 1
                val childJob1 = launch {
                    Log.i(TAG, "子协程 1 执行开始")
                    try {
                        delay(2000)
                    }finally {
                        withContext(NonCancellable){
                            Log.i(TAG, "子协程 1 执行 finally 代码")
                            delay(1000)
                            Log.i(TAG, "子协程 1 执行 finally 代码结束")
                        }
                    }
                }

                // 子协程 2
                val childJob2 = launch {
                    Log.i(TAG, "子协程 2 执行开始")
                    delay(100)
                    Log.i(TAG, "子协程 2 抛出 IllegalArgumentException 异常")
                    throw IllegalArgumentException()
                }

                // 运行时 子协程 2 会先抛出异常 , 此时 子协程 1 也会被取消
                // 父协程 会在 两个协程都取消后 才会处理异常
            }

            // 等待父协程执行完毕
            job.join()
            Log.i(TAG, "父协程执行完毕")
        }
    }
}

执行结果 : 由下面的日志可知 ,

  • 子协程 1 没有执行完 2 秒 , 就被 子协程 2 的异常打断了 ,
  • 但是 子协程 1 中的 finally 代码中的 1 秒执行完毕了 ;
  • 子协程 2 早早抛出异常退出了 , 子协程 1 还执行了 1 秒 ,
  • 最后 父协程 等 子协程 1 执行完毕后 , 才处理的 子协程 2 抛出的 异常 ;
00:07:35.258  I  父协程开始执行
00:07:35.262  I  子协程 1 执行开始
00:07:35.270  I  子协程 2 执行开始
00:07:35.427  I  子协程 2 抛出 IllegalArgumentException 异常
00:07:35.467  I  子协程 1 执行 finally 代码
00:07:36.484  I  子协程 1 执行 finally 代码结束
00:07:36.504  I  CoroutineExceptionHandler 中处理异常 
                 协程上下文 [kim.hsl.coroutine.MainActivity$onCreate$1$invokeSuspend$$inlined$CoroutineExceptionHandler$1@f30fe8, StandaloneCoroutine{Cancelling}@bc6a601, Dispatchers.Default]
                 异常内容 java.lang.IllegalArgumentException
00:07:36.516  I  父协程执行完毕

在这里插入图片描述





二、异常聚合 ( 多个子协程抛出的异常会聚合到第一个异常中 )



父协程有多个 子协程 , 这些子协程 都 抛出了 异常 , 此时 只会处理 第一个 异常 ;

这是因为 多个 子协程 , 如果出现了多个异常 , 从第二个异常开始 , 都会将异常绑定到第一个异常上面 ;


在 CoroutineExceptionHandler 中 , 调用 throwable.suppressed.contentToString() 可以获取多个异常 , 被绑定的异常会存放到一个数组中 , 有多少个异常都会显示出来 ;


代码示例 :

package kim.hsl.coroutine

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.*

class MainActivity : AppCompatActivity() {
    val TAG = "MainActivity"
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        runBlocking {
            // 创建 协程异常处理器 CoroutineExceptionHandler
            val coroutineExceptionHandler = CoroutineExceptionHandler {
                    coroutineContext, throwable ->

                Log.i(TAG, "CoroutineExceptionHandler 中处理异常 " +
                        "\n协程上下文 ${coroutineContext}" +
                        "\n异常内容 ${throwable} " +
                        // 这是一个数组 , 不管有多少个异常 , 都会打印出来
                        "\n第二个异常内容 ${throwable.suppressed.contentToString()}")
            }

            // 父协程
            val job = GlobalScope.launch(coroutineExceptionHandler) {
                Log.i(TAG, "父协程开始执行")

                // 子协程 1
                val childJob1 = launch {
                    Log.i(TAG, "子协程 1 执行开始")
                    try {
                        delay(2000)
                    }finally {
                        Log.i(TAG, "子协程 1 抛出 IllegalArgumentException 异常 ( 第二个异常 )")
                        throw IllegalArgumentException()
                    }
                }

                // 子协程 2
                val childJob2 = launch {
                    Log.i(TAG, "子协程 2 执行开始")
                    delay(100)
                    Log.i(TAG, "子协程 2 抛出 ArithmeticException 异常 ( 第一个异常 )")
                    throw ArithmeticException()
                }

                // 运行时 子协程 2 会先抛出异常 , 此时 子协程 1 也会被取消 , 在 finally 中抛出异常
                // 父协程 会在 两个协程都取消后 才会处理异常
                // 第二个异常 会被 绑定到 第一个异常 上
            }

            // 等待父协程执行完毕
            job.join()
            Log.i(TAG, "父协程执行完毕")
        }
    }
}

执行结果 :

00:46:21.239  I  父协程开始执行
00:46:21.243  I  子协程 1 执行开始
00:46:21.245  I  子协程 2 执行开始
00:46:21.387  I  子协程 2 抛出 ArithmeticException 异常 ( 第一个异常 )
00:46:21.390  I  子协程 1 抛出 IllegalArgumentException 异常 ( 第二个异常 )
00:46:21.490  I  CoroutineExceptionHandler 中处理异常 
                 协程上下文 [kim.hsl.coroutine.MainActivity$onCreate$1$invokeSuspend$$inlined$CoroutineExceptionHandler$1@bc6a601, StandaloneCoroutine{Cancelling}@fef2ca6, Dispatchers.Default]
                 异常内容 java.lang.ArithmeticException 
                 第二个异常内容 [java.lang.IllegalArgumentException]
00:46:21.492  I  父协程执行完毕

在这里插入图片描述