• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

Kotlin 协程的异常处理

武飞扬头像
xiangxiongfly915
帮助2

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 捕获整个作用域的所有异常。

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgcbkec
系列文章
更多 icon
同类精品
更多 icon
继续加载