协程入门(五):异常处理

4,274 阅读3分钟

关键点

1.子协程未被捕获的异常会抛出到父协程,如果父协程没捕获异常,会导致父协程终止

2.launch/actor内发生的未被捕获的异常不会传递到父协程之外的地方

3.async/produce内发生的未被捕获的异常会传递到启动其父协程所在的线程

4.android原生的try catch无法捕获到不同线程抛出的异常

异常的传播

在协程中,不同的启动方式,对异常的传播处理不一样。

对于launch和actor构建器是不传播异常的,async和produce是传播异常的。 所谓的传播异常,是指能够将异常主动往外抛到启动顶层协程所在的线程; 传播异常表示不在本协程所在线程发生,异常直接往外抛到启动该协程所在的线程。

异常传播示例

不传播异常

globalScopeLaunch方式启动的是顶层协程,本身不存在父协程,在里面发生异常后, 只会再logCat输出异常异常,并不会影响到外部线程的运行。

    fun globalScopeLaunch()  {
        println("start")
        GlobalScope.launch {
            println("launch Throwing exception")
            throw NullPointerException()
        }
        Thread.sleep(3000)
        //GlobalScope.launch产生的异常不影响该线程执行
        println("end")
    }

打印出:
    start
    launch Throwing exception
    Exception in thread "DefaultDispatcher-worker-1 @coroutine#1" java.lang.NullPointerException.....
    end

globalScopeLaunch方式启动的是顶层协程,本身不存在父协程,在里面发生异常后, 并不会影响到外部协程的运行,无论是否运行在同一个线程。


    fun globalScopeLaunch()  = runBlocking {
        println("start ${Thread.currentThread().id}")
        GlobalScope.launch {
            println("launch Throwing exception ${Thread.currentThread().id}")
            throw NullPointerException()
        }.join()

        GlobalScope.launch(start = CoroutineStart.UNDISPATCHED) {
            println("launch UNDISPATCHED Throwing exception ${Thread.currentThread().id}")
            throw IndexOutOfBoundsException()
        }.join()

        //GlobalScope.launch产生的异常不影响该协程执行
        println("end ${Thread.currentThread().id}")
    }
    
打印出:
    start 1
    launch Throwing exception 11
    Exception in thread "DefaultDispatcher-worker-1 @coroutine#2" java.lang.NullPointerException...  异常打印
    launch UNDISPATCHED Throwing exception 1
    Exception in thread "main @coroutine#1" java.lang.IndexOutOfBoundsException...  异常打印
    end 1

传播异常

async内发生的异常可以被抛出到启动该协程所在的线程上,但是需要显示使用await()方法才可以捕获到该异常

    fun globalScopeAsync()  = runBlocking {
        println("start ${Thread.currentThread().id}")
        val deferred = GlobalScope.async {
            println("async Throwing exception ${Thread.currentThread().id}")
            throw NullPointerException()
        }
        //deferred.join()
        deferred.await()
        //join不会接收异常可以正常执行下面步骤,await会接收到抛出的异常导致后面步骤无法执行
        //不使用join和await则可以正常执行下面步骤
        println("end ${Thread.currentThread().id}")
        delay(3000)
    }
    
使用join打印出:
     start 1
     async Throwing exception 11
     end 1
     
使用await打印出:
     start 1
     async Throwing exception 12
     java.lang.NullPointerException....抛出的异常
     
不使用join和await打印出:
     start 1
     end 1
     async Throwing exception 11

也就意味着不在协程环境下启动async后,其内部抛出的异常无法被外部线程捕获,因为await需要在协程环境下才能调用

    fun globalScopeAsync()  {
        println("start")
        val deferred = GlobalScope.async {
            println("async Throwing exception")
            throw NullPointerException()
        }
        Thread.sleep(3000)
        //非协程环境无法使用await,上边产生的异常不影响该线程执行
        println("end")
    }
    
打印出:
    start
    async Throwing exception
    end

子协程未捕获的异常会导致父协程终止

子协程(无论是否与父协程在同一线程,async或launch启动)未被捕获的异常都会传播到父协程,导致父协程终止

    fun launchChildException()  = runBlocking {
        println("start ${Thread.currentThread().id}")
        //这边换成async也是一样结果
        launch {
            println("launch Throwing exception ${Thread.currentThread().id}")
            throw NullPointerException()
        }
        delay(3000)
        //因为子协程出现异常,默认会导致父协程终止,下边不会被执行
        println("end ${Thread.currentThread().id}")
    }
    
打印出:
    start 1
    launch Throwing exception 1
    java.lang.NullPointerException....

    fun launchChildExceptionIO()  = runBlocking {
        println("start ${Thread.currentThread().id}")
        //这边换成async也是一样结果
        launch(Dispatchers.IO) {
            println("launch Throwing exception ${Thread.currentThread().id}")
            throw NullPointerException()
        }
        delay(3000)
        //因为子协程出现异常,默认会导致父协程终止,下边不会被执行
        println("end ${Thread.currentThread().id}")
    }
    
打印出:
   start 1
   launch Throwing exception 11
   java.lang.NullPointerException....

异常捕获示例

使用try{}catch{}捕获

对deferred.await()进行try catch能正常捕获到错误, 但是对join try catch等同于没有。 这也意味着只有async的中发生的异常,才存在被try catch的可能。

    fun asyncCatchException()  = runBlocking {
        println("start ${Thread.currentThread().id}")
        val deferred =  GlobalScope.async {
            println("async Throwing exception ${Thread.currentThread().id}")
            throw NullPointerException()
        }
        try {
            //await能够被try catch捕获错误,从而继续玩下执行
            deferred.await()
            //join本身忽视错误,有没有try catch都一样
            //deferred.join()
        } catch (exception : NullPointerException) {
            println("catch exception")
        }
        println("end ${Thread.currentThread().id}")
        delay(3000)
    }
    
await打印出:
    start 1
    async Throwing exception 11
    catch exception
    end 1
     
但是对join打印出:
    start 1
    async Throwing exception 12
    end 1

CoroutineExceptionHandler捕获

对于launch这种不传播异常的协程,try catch无法捕获,可以使用CoroutineExceptionHandler, CoroutineExceptionHandler无法捕获async内的异常

    fun coroutineExceptionHandler ()  = runBlocking {

        val handleException = CoroutineExceptionHandler { _, throwable ->
            //可以捕获到launch中抛出的异常,但是不能捕获async
            println("CoroutineExceptionHandler catch $throwable") 
        }
        println("start ${Thread.currentThread().id}")
        val job =  GlobalScope.launch(handleException) {
            println("launch Throwing exception ${Thread.currentThread().id}")
            throw NullPointerException()
        }
        
        //job.join()
        println("end ${Thread.currentThread().id}")
        delay(3000)
    }
    
打印出:
    launch/launch.join打印出:
    start 1
    launch Throwing exception 11
    CoroutineExceptionHandler catch java.lang.NullPointerException
    end 1

CoroutineExceptionHandler无法捕获async内的异常,无论有没调用await()

    fun asyncCoroutineExceptionHandler ()  = runBlocking {

        val handleException = CoroutineExceptionHandler { _, throwable ->
             //可以捕获到launch中抛出的异常,但是不能捕获async
            println("CoroutineExceptionHandler catch $throwable") 
        }
        println("start ${Thread.currentThread().id}")
        val referred =  GlobalScope.async(handleException) {
            println("async Throwing exception ${Thread.currentThread().id}")
            throw IndexOutOfBoundsException()
        }
      //  referred.join()
     //   referred.await()
        println("end ${Thread.currentThread().id}")
        delay(3000)
    }
    
await打印出:
    start 1
    async Throwing exception 11
    java.lang.IndexOutOfBoundsException...
    
没调用await打印:
    start 1
    async Throwing exception 11
    end 1

特殊的CancellationException

处于delay的协程再被cancel()后会抛出CancellationException异常,该异常不被捕获也不会引起父协程的终止,也无法被CoroutineExceptionHandler捕获

    fun launchCancelException()  = runBlocking {
        val handleException = CoroutineExceptionHandler { _, throwable ->
            //无法捕获到launch中抛出的CancellationException异常
            println("CoroutineExceptionHandler catch $throwable")
        }
        println("start ${Thread.currentThread().id}")
        launch(handleException) {
            println("launch Throwing exception ${Thread.currentThread().id}")
            //不会导致父协程终止
            throw CancellationException()
        }
        delay(3000)
        //因为子协程出现异常,默认会导致父协程终止,但CancellationException不会
        println("end ${Thread.currentThread().id}")
    }
    
打印出:
    start 1
    launch Throwing exception 1
    end 1

但是如果使用了await则异常会抛出在外部线程,需要进行捕获,否则会导致启动协程的线程崩溃

    fun asyncCancelException()  = runBlocking {
        val handleException = CoroutineExceptionHandler { _, throwable ->
            //无法捕获到launch中抛出的CancellationException异常
            println("CoroutineExceptionHandler catch $throwable")
        }
        println("start ${Thread.currentThread().id}")
        val deferred = async(handleException) {
            println("async Throwing exception ${Thread.currentThread().id}")
            //不会导致父协程终止
            throw CancellationException()
        }
        //导致CancellationException异常抛出到该线程,下边无法执行
        deferred.await()
        //若没有await则下边可以正常执行
        delay(3000)
        println("end ${Thread.currentThread().id}")
    }
    
打印出:
    start 1
    async Throwing exception 1
    end 1
    
使用了await,打印出:
    start 1
    async Throwing exception 1
    java.util.concurrent.CancellationException..

微信公众号