[Kotlin Tutorials 22] 協程中的異常處理

来源:https://www.cnblogs.com/mengdd/archive/2023/06/08/kotlin-coroutine-exception-handling.html
-Advertisement-
Play Games

# 協程中的異常處理 ![coroutine exception handling](https://img2023.cnblogs.com/blog/325852/202306/325852-20230608084235670-684439238.png) ## Parent-Child關係 如果 ...


協程中的異常處理

coroutine exception handling

Parent-Child關係

如果一個coroutine拋出了異常, 它將會把這個exception向上拋給它的parent, 它的parent會做以下三件事情:

  • 取消其他所有的children.
  • 取消自己.
  • 把exception繼續向上傳遞.

這是預設的異常處理關係, 取消是雙向的, child會取消parent, parent會取消所有child.

catch不住的exception

看這個代碼片段:

fun main() {
    val scope = CoroutineScope(Job())
    try {
        scope.launch {
            throw RuntimeException()
        }
    } catch (e: Exception) {
        println("Caught: $e")
    }

    Thread.sleep(100)
}

這裡的異常catch不住了.
會直接讓main函數的主進程崩掉.

這是因為和普通的異常處理機制不同, coroutine中未被處理的異常並不是直接拋出, 而是按照job hierarchy向上傳遞給parent.

如果把try放在launch裡面還行.

預設的異常處理

預設情況下, child發生異常, parent和其他child也會被取消.

fun main() {
    println("start")
    val exceptionHandler = CoroutineExceptionHandler { _, exception ->
        println("CoroutineExceptionHandler got $exception")
    }
    val scope = CoroutineScope(Job() + exceptionHandler)

    scope.launch {
        println("child 1")
        delay(1000)
        println("finish child 1")
    }.invokeOnCompletion { throwable ->
        if (throwable is CancellationException) {
            println("Coroutine 1 got cancelled!")
        }
    }

    scope.launch {
        println("child 2")
        delay(100)
        println("child 2 throws exception")
        throw RuntimeException()
    }

    Thread.sleep(2000)
    println("end")
}

列印出:

start
child 1
child 2
child 2 throws exception
Coroutine 1 got cancelled!
CoroutineExceptionHandler got java.lang.RuntimeException
end

SupervisorJob

如果有一些情形, 開啟了多個child job, 但是卻不想因為其中一個的失敗而取消其他, 怎麼辦? 用SupervisorJob.

比如:

val uiScope = CoroutineScope(SupervisorJob())

如果你用的是scope builder, 那麼用supervisorScope.

SupervisorJob改造上面的例子:

fun main() {
    println("start")
    val exceptionHandler = CoroutineExceptionHandler { _, exception ->
        println("CoroutineExceptionHandler got $exception")
    }
    val scope = CoroutineScope(SupervisorJob() + exceptionHandler)

    scope.launch {
        println("child 1")
        delay(1000)
        println("finish child 1")
    }.invokeOnCompletion { throwable ->
        if (throwable is CancellationException) {
            println("Coroutine 1 got cancelled!")
        }
    }

    scope.launch {
        println("child 2")
        delay(100)
        println("child 2 throws exception")
        throw RuntimeException()
    }
    Thread.sleep(2000)
    println("end")
}

輸出:

start
child 1
child 2
child 2 throws exception
CoroutineExceptionHandler got java.lang.RuntimeException
finish child 1
end

儘管coroutine 2拋出了異常, 另一個coroutine還是做完了自己的工作.

SupervisorJob的特點

SupervisorJob把取消變成了單向的, 只能從上到下傳遞, 只能parent取消child, 反之不能取消.
這樣既顧及到了由於生命周期的結束而需要的正常取消, 又避免了由於單個的child失敗而取消所有.

viewModelScope的context就是用了SupervisorJob() + Dispatchers.Main.immediate.

除了把取消變為單向的, supervisorScope也會和coroutineScope一樣等待所有child執行結束.

supervisorScope中直接啟動的coroutine是頂級coroutine.
頂級coroutine的特性:

  • 可以加exception handler.
  • 自己處理exception.
    比如上面的例子中coroutine child 2可以直接加exception handler.

使用註意事項, SupervisorJob只有兩種寫法:

  • 作為CoroutineScope的參數傳入: CoroutineScope(SupervisorJob()).
  • 使用supervisorScope方法.

把Job作為coroutine builder(比如launch)的參數傳入是錯誤的做法, 不起作用, 因為一個新的coroutine總會assign一個新的Job.

異常處理的辦法

try-catch

和普通的異常處理一樣, 我們可以用try-catch, 只是註意要在coroutine裡面:

fun main() {
    val scope = CoroutineScope(Job())
    scope.launch {
        try {
            throw RuntimeException()
        } catch (e: Exception) {
            println("Caught: $e")
        }
    }

    Thread.sleep(100)
}

這樣就能列印出:

Caught: java.lang.RuntimeException

對於launch, try要包住整塊.
對於async, try要包住await語句.

scope function: coroutineScope()

coroutineScope會把其中未處理的exception拋出來.

相比較於這段代碼中catch不到的exception:

fun main() {
    val scope = CoroutineScope(Job())
    scope.launch {
        try {
            launch {
                throw RuntimeException()
            }
        } catch (e: Exception) {
            println("Caught: $e")
        }
    }
    Thread.sleep(100)
}

沒走到catch里, 仍然是主進程崩潰.

這個exception是可以catch到的:

fun main() {
    val scope = CoroutineScope(Job())
    scope.launch {
        try {
            coroutineScope {
                launch {
                    throw RuntimeException()
                }
            }
        } catch (e: Exception) {
            println("Caught: $e")
        }
    }

    Thread.sleep(100)
}

列印出:

Caught: java.lang.RuntimeException

因為這裡coroutineScope把異常又重新拋出來了.

註意這裡換成supervisorScope可是不行的.

CoroutineExceptionHandler

CoroutineExceptionHandler是異常處理的最後一個機制, 此時coroutine已經結束了, 在這裡的處理通常是報告log, 展示錯誤等.
如果不加exception handler那麼unhandled exception會進一步往外拋, 如果最後都沒人處理, 那麼可能造成進程崩潰.

CoroutineExceptionHandler需要加在root coroutine上.

這是因為child coroutines會把異常處理代理到它們的parent, 後者繼續代理到自己的parent, 一直到root.
所以對於非root的coroutine來說, 即便指定了CoroutineExceptionHandler也沒有用, 因為異常不會傳到它.

兩個例外:

  • async的異常在Deferred對象中, CoroutineExceptionHandler也沒有任何作用.
  • supervision scope下的coroutine不會向上傳遞exception, 所以CoroutineExceptionHandler不用加在root上, 每個coroutine都可以加, 單獨處理.

通過這個例子可以看出另一個特性: CoroutineExceptionHandler只有當所有child都結束之後才會處理異常信息.

@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception -> 
        println("CoroutineExceptionHandler got $exception") 
    }
    val job = GlobalScope.launch(handler) {
        launch { // the first child
            try {
                delay(Long.MAX_VALUE)
            } finally {
                withContext(NonCancellable) {
                    println("Children are cancelled, but exception is not handled until all children terminate")
                    delay(100)
                    println("The first child finished its non cancellable block")
                }
            }
        }
        launch { // the second child
            delay(10)
            println("Second child throws an exception")
            throw ArithmeticException()
        }
    }
    job.join()
}

輸出:

Second child throws an exception
Children are cancelled, but exception is not handled until all children terminate
The first child finished its non cancellable block
CoroutineExceptionHandler got java.lang.ArithmeticException

如果多個child都拋出異常, 只有第一個被handler處理, 其他都在exception.suppressed欄位里.

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("CoroutineExceptionHandler got $exception with suppressed ${exception.suppressed.contentToString()}")
    }
    val job = GlobalScope.launch(handler) {
        launch {
            try {
                delay(Long.MAX_VALUE) // it gets cancelled when another sibling fails with IOException
            } finally {
                throw ArithmeticException() // the second exception
            }
        }
        launch {
            delay(100)
            throw IOException() // the first exception
        }
        delay(Long.MAX_VALUE)
    }
    job.join()
}

輸出:

CoroutineExceptionHandler got java.io.IOException with suppressed [java.lang.ArithmeticException]

單獨說一下async

async比較特殊:

  • 作為top coroutine時, 在await的時候try-catch異常.
  • 如果是非top coroutine, async塊里的異常會被立即拋出.

例子:

fun main() {
    val scope = CoroutineScope(SupervisorJob())
    val deferred = scope.async {
        throw RuntimeException("RuntimeException in async coroutine")
    }

    scope.launch {
        try {
            deferred.await()
        } catch (e: Exception) {
            println("Caught: $e")
        }
    }

    Thread.sleep(100)
}

這裡由於用了SupervisorJob, 所以async是top coroutine.

fun main() {

    val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, exception ->
        println("Handle $exception in CoroutineExceptionHandler")
    }

    val topLevelScope = CoroutineScope(SupervisorJob() + coroutineExceptionHandler)
    topLevelScope.launch {
        async {
            throw RuntimeException("RuntimeException in async coroutine")
        }
    }
    Thread.sleep(100)
}

當它不是top coroutine時, 異常會被直接拋出.

特殊的CancellationException

CancellationException是特殊的exception, 會被異常處理機制忽略, 即便拋出也不會向上傳遞, 所以不會取消它的parent.
但是CancellationException不能被catch, 如果它不被拋出, 其實協程沒有被成功cancel, 還會繼續執行.

CancellationException的透明特性:
如果CancellationException是由內部的其他異常引起的, 它會向上傳遞, 並且把原始的那個異常傳遞上去.

@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("CoroutineExceptionHandler got $exception")
    }
    val job = GlobalScope.launch(handler) {
        val inner = launch { // all this stack of coroutines will get cancelled
            launch {
                launch {
                    throw IOException() // the original exception
                }
            }
        }
        try {
            inner.join()
        } catch (e: CancellationException) {
            println("Rethrowing CancellationException with original cause")
            throw e // cancellation exception is rethrown, yet the original IOException gets to the handler
        }
    }
    job.join()
}

輸出:

Rethrowing CancellationException with original cause
CoroutineExceptionHandler got java.io.IOException

這裡Handler拿到的是最原始的IOException.

Further Reading

官方文檔:

Android官方文檔上鏈接的博客和視頻:

其他:

作者: 聖騎士Wind
出處: 博客園: 聖騎士Wind
Github: https://github.com/mengdd
微信公眾號: 聖騎士Wind
微信公眾號: 聖騎士Wind
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • # 系統架構 **主題topic和分區partition** - topic Kafka中存儲數據的邏輯分類;你可以理解為資料庫中“表”的概念;比如,將app端日誌、微信小程式端日誌、業務庫訂單表數據分別放入不同的topic - partition分區(提升kafka吞吐量) topic中數據的具體 ...
  • 區別於通過發行版自帶的倉庫, 介紹如何通過 targz 文件安裝 Elastic Search 服務, 使用的 Linux 為 Centos 7 ...
  • ### FAQ #### 畫出 MySQL 的基本架構圖 ![image.png](https://cdn.nlark.com/yuque/0/2023/png/559966/1686211777836-612d0e7c-7595-44b5-ad5c-9392633de905.png#average ...
  • ### wait_timeout and interactive_timeout 參數 - 非交互模式連接:通常情況下,應用到RDS實例會採用非交互模式,具體採用哪個模式需要查看應用的連接方式配置,比如PHP通過傳遞MYSQL_CLIENT_INTERACTIVE常量給mysql_connect() ...
  • 摘要:合理地管理和分配系統資源,是保證資料庫系統穩定高效運行的關鍵。 本文分享自華為雲社區《GaussDB(DWS)資源管理能力介紹與應用示例》,作者: 門前一棵葡萄樹 。 一、資源管理能力 1.1 概述 資料庫運行過程中使用的公共資源包含:系統資源(CPU、記憶體、網路等)和資料庫共用資源(鎖、計數 ...
  • 為了保證統計數據的準確性,比如訂單金額,一個常用的方法是在查詢時增加final關鍵字。那final關鍵字是如何合併數據的,以及合併的數據範圍是怎樣的,本文就對此做一個簡單的探索。 ...
  • 資料庫產品的成功絕對不是技術堆疊的成功,而是需要有大量的應用場景磨合才能逐步成功的。如果僅僅依靠自己那幾百個用戶,想要發展出成熟的高水平的商用資料庫產品來,那幾乎是不太能的。依靠開源社區的廣大用戶來研發自己的資料庫產品不失為一種比較好的策略。 ...
  • ## 配置AOSP docker編譯環境 ### 1.安裝docker ``` curl -fsSL https://get.docker.com -o get-docker.sh sh get-docker.sh ``` 參考:[github](https://github.com/docker/ ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...