IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Kotlin 协程的异常处理 -> 正文阅读

[移动开发]Kotlin 协程的异常处理

Kotlin 协程的异常处理

概述

协程是互相协作的程序,协程是结构化的。

如果把Java的异常处理机制,照搬到Kotlin协程中,一定会遇到很多的坑。

Kotlin协程中的异常主要分两大类

  • 协程取消异常(CancellationException)
  • 其他异常

异常处理六大准则

  • 协程的取消需要内部配合
  • 不要轻易打破协程的父子结构
  • 捕获了CancellationException后,需要考虑是否重新抛出来
  • 不要用try-catch直接包裹launchasync
  • 灵活使用SurpervisorJob,控制异常传播的范围
  • 使用CoroutineExceptionHandler处理复杂结构的协程异常,仅在顶层协程中起作用

核心理念:协程是结构化的,异常传播也是结构化的。

准则一:协程的取消需要内部配合

问题:cancel不被响应

fun main() = runBlocking {
    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")
}

/*
输出结果:
i= 1
i= 2
i= 3
... //不会停止,一直执行
*/

解决:使用isActive判断是否处于活动状态

fun main() = 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")
}

/*
输出结果:
i= 1
i= 2
i= 3
i= 4
end
*/

准则二:不要打破协程的父子结构

问题:子协程不会跟随父协程一起取消

协程的优势在于结构化并发,它的许多特性都是建立这个特性之上的,如果我们无意中打破了它的父子结构关系,就会导致协程代码无法按照预期执行。

val fixedDispatcher = Executors.newFixedThreadPool(2) {
    Thread(it, "MyFixedThread").apply { isDaemon = false }
}.asCoroutineDispatcher()

fun main() = runBlocking {
    val parentJob = launch(fixedDispatcher) {
        launch(Job()) {
            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")
}

/*
输出结果:
子协程2:i= 1
子协程1:i= 1
子协程2:i= 2
子协程1:i= 2
子协程1:i= 3
子协程2:i= 3
子协程2:i= 4
子协程1:i= 4
end
子协程1:i= 5
子协程1:i= 6
...... //子协程1不会停止
*/

说明:协程是结构化的,通常情况下,当取消了父协程,子协程也会被取消。但是在这里,子协程1不再是parentJob的子协程,打破了原有的结构化关系。

解决:不破坏父子结构

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需要重新抛出来

准则一中的Thread.sleep可以替代为delaydelay()函数可以自动检测当前的协程是否已经被取消,如果已经被取消,则会抛出CancellationException异常,从而终止当前的协程。

协程是CancellationException异常来实现结构化取消的,有的时候我们出于某些目的需要捕获CancellationException异常,但捕获完以后,还需要考虑是否重新抛出来。

问题:捕获CancellationException导致崩溃

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异常
...... //程序不会终止
*/

解决:需要重新抛出

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
*/

准则四:不要用try-catch直接包裹launch、async

问题:try-catch不起作用

协程的代码执行顺序与普通程序不一样,直接使用try-catch包裹launch、async是不会有任何效果的。

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
*/

解决:调整作用域

可以将try-catch移动到协程体内部,这样可以捕获到异常了。

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

普通Job,当子Job发生异常时,会导致parentJob取消,从而导致其他子Job也受到牵连,这也是协程结构化的体现。

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
*/

解决:使用SupervisorJob

SurpervisorJobJob的子类,SurpervisorJob是一个种特殊的Job,可以控制异常的传播范围,当子Job发生异常时,其他的子Job不会受到影响。

在这里插入图片描述

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
*/

解决:使用supervisorScope

supervisorScope底层使用是SupervisorJob

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提供了CoroutineExceptionHandler处理复杂的协程嵌套结构,可以捕获整个作用域内的所有异常,只在顶层协程中起作用。

问题:处理复杂结构的协程异常

解决:使用CoroutineExceptionHandler

fun main() = runBlocking {
    val myExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
        println("捕获异常:$throwable")
    }
    val scope = CoroutineScope(SupervisorJob() + myExceptionHandler)

    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")
}
/*
输出结果:
捕获异常:java.lang.ArithmeticException: / by zero
hello world 222
hello world 333
end
*/

总结

  • 准则一:协程的取消需要内部的配合。
  • 准则二:不要轻易打破协程的父子结构。协程的优势在于结构化并发,他的许多特性都是建立在这之上的,如果打破了它的父子结构,会导致协程无法按照预期执行。
  • 准则三:捕获 CancellationException 异常后,需要考虑是否重新抛出来。协程是依赖 CancellationException 异常来实现结构化取消的,捕获异常后需要考虑是否重新抛出来。
  • 准则四:不要用 try-catch 直接包裹 launch、async。协程代码的执行顺序与普通程序不一样,直接使用 try-catch 可能不会达到预期效果。
  • 准则五:使用 SupervisorJob 控制异常传播范围。SupervisorJob 是一种特殊的 Job,可以控制异常的传播范围,不会受到子协程中的异常而取消自己。
  • 准则六:使用 CoroutineExceptionHandler 捕获异常。当协程嵌套层级比较深时,可以在顶层协程中定义 CoroutineExceptionHandler 捕获整个作用域的所有异常。
  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-04-09 18:32:44  更:2022-04-09 18:34:17 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 20:38:47-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码