[譯] 關於 Kotlin Coroutines, 你可能會犯的 7 個錯誤

原文做者:Lukas Lechnerhtml

原文地址:7 common mistakes you might be making when using Kotlin Coroutinesjava

譯者:秉心說git

在我看來,Kotlin Coroutines(協程) 大大簡化了同步和異步代碼。可是,我發現了許多開發者在使用協程時會犯一些通用性的錯誤。github

1. 在使用協程時實例化一個新的 Job 實例

有時候你會須要一個 job 來對協程進行一些操做,例如,稍後取消。另外因爲協程構建器 launch{}async{} 都須要 job 做爲入參,你可能會想到建立一個新的 job 實例做爲參數來使用。這樣的話,你就擁有了一個 job 引用,稍後你能夠調用它的 .cancel() 方法。數據庫

fun main() = runBlocking {

    val coroutineJob = Job()
    launch(coroutineJob) {
        println("performing some work in Coroutine")
        delay(100)
    }.invokeOnCompletion { throwable ->
        if (throwable is CancellationException) {
            println("Coroutine was cancelled")
        }
    }

    // cancel job while Coroutine performs work
    delay(50)
    coroutineJob.cancel()
}
複製代碼

這段代碼看起來沒有任何問題,協程被成功取消了。安全

>_ 

performing some work in Coroutine
Coroutine was cancelled

Process finished with exit code 0
複製代碼

可是,讓咱們試試在協程做用域 CoroutineScope 中運行這個協程,而後取消協程做用域而不是協程的 jobmarkdown

fun main() = runBlocking {

    val scopeJob = Job()
    val scope = CoroutineScope(scopeJob)

    val coroutineJob = Job()
    scope.launch(coroutineJob) {
        println("performing some work in Coroutine")
        delay(100)
    }.invokeOnCompletion { throwable ->
        if (throwable is CancellationException) {
            println("Coroutine was cancelled")
        }
    }

    // cancel scope while Coroutine performs work
    delay(50)
    scope.cancel()
}
複製代碼

看成用域被取消時,它內部的全部協程都會被取消。可是當咱們再次執行修改過的代碼時,狀況並非這樣。網絡

>_

performing some work in Coroutine

Process finished with exit code 0
複製代碼

如今,協程沒有被取消,Coroutine was cancelled 沒有被打印。併發

爲何會這樣?app

原來,爲了讓異步/同步代碼更加安全,協程提供了革命性的特性 —— 「結構化併發」 。「結構化併發」 的一個機制就是:看成用域被取消時,就取消該做用域中的全部協程。爲了保證這一機制正常工做,做用域的 job 和協程的 job 以前的層級結構以下圖所示:

在咱們的例子中,發生了一些異常狀況。經過向協程構建器 launch() 傳遞咱們本身的 job 實例,實際上並無把新的 job 實例和協程自己進行綁定,取而代之的是,它成爲了新協程的父 job。因此你建立的新協程的父 job 並非協程做用域的 job,而是新建立的 job 對象。

所以,協程的 job 和協程做用域的 job 此時並無什麼關聯。

咱們打破告終構化併發,所以當咱們取消協程做用域時,協程將再也不被取消。

解決方式是直接使用 launch() 返回的 job

fun main() = runBlocking {
    val scopeJob = Job()
    val scope = CoroutineScope(scopeJob)

    val coroutineJob = scope.launch {
        println("performing some work in Coroutine")
        delay(100)
    }.invokeOnCompletion { throwable ->
        if (throwable is CancellationException) {
            println("Coroutine was cancelled")
        }
    }

    // cancel while coroutine performs work
    delay(50)
    scope.cancel()
}
複製代碼

這樣,協程就能夠隨着做用域的取消而取消了。

>_

performing some work in Coroutine
Coroutine was cancelled

Process finished with exit code 0
複製代碼

2. 錯誤的使用 SupervisorJob

有時候你會使用 SupervisorJob 來達到下面的效果:

  1. 在 job 繼承體系中中止異常向上傳播
  2. 當一個協程失敗時不影響其餘的同級協程

因爲協程構建器 launch{}async{} 均可以傳遞 Job 做爲入參,因此你能夠考慮向構建器傳遞 SupervisorJob 實例。

launch(SupervisorJob()){
    // Coroutine Body
}
複製代碼

可是,就像錯誤 1 ,這樣會打破結構化併發的取消機制。正確的解決方式是使用 supervisorScope{} 做用域函數。

supervisorScope {
    launch {
        // Coroutine Body
    }
}
複製代碼

3. 不支持取消

當你在本身定義的 suspend 函數中進行一些比較重的操做時,例如計算斐波拉契數列:

// factorial of n (n!) = 1 * 2 * 3 * 4 * ... * n
suspend fun calculateFactorialOf(number: Int): BigInteger =
    withContext(Dispatchers.Default) {
        var factorial = BigInteger.ONE
        for (i in 1..number) {
            factorial = factorial.multiply(BigInteger.valueOf(i.toLong()))
        }
        factorial
    }
複製代碼

這個掛起函數有一個問題:它不支持 「合做式取消」 。這意味着即便執行這個函數的協程被提早取消了,它仍然會繼續運行直到計算完成。爲了不這種狀況,能夠按期執行如下函數:

下面的代碼使用了 ensureActive() 來支持取消。

// factorial of n (n!) = 1 * 2 * 3 * 4 * ... * n
suspend fun calculateFactorialOf(number: Int): BigInteger =
    withContext(Dispatchers.Default) {
        var factorial = BigInteger.ONE
        for (i in 1..number) {
            ensureActive()
            factorial = factorial.multiply(BigInteger.valueOf(i.toLong()))
        }
        factorial
    }
複製代碼

Kotlin 標準庫中的掛起函數(如 delay()) 都是能夠配合取消的。可是對於你本身的掛起函數,不要忘記考慮取消的狀況。

4. 進行網絡請求或者數據庫查詢時切換調度器

這一項並不真的是一個 「錯誤」 ,可是仍可能讓你的代碼難以理解,甚至更加低效。一些開發者認爲當調用協程時,就應該切換到後臺調度器,例如,進行網絡請求的 Retrofit 的 suspend 函數,進行數據庫操做的 Room 的 suspend 函數。

這並非必須的。由於全部的掛起函數都應該是主線程安全的,Retrofit 和 Room 都遵循了這一約定。你能夠閱讀個人 這篇文章 以瞭解更多內容。

5. 嘗試使用 try/catch 來處理協程的異常

協程的異常處理很複雜,我花了至關多的時間才徹底理解,並經過 博客講座 向其餘開發者進行了解釋。我還做了一些 來總結這個複雜的話題。

關於 Kotlin 協程異常處理最不直觀的方面之一是,你不能使用 try-catch 來捕獲異常。

fun main() = runBlocking<Unit> {
    try {
        launch {
            throw Exception()
        }
    } catch (exception: Exception) {
        println("Handled $exception")
    }
}
複製代碼

若是運行上面的代碼,異常不會被處理而且應用會 crash 。

>_ 

Exception in thread "main" java.lang.Exception

Process finished with exit code 1
複製代碼

Kotlin Coroutines 讓咱們能夠用傳統的編碼方式書寫異步代碼。可是,在異常處理方面,並無如大多數開發者想的那樣使用傳統的 try-catch 機制。若是你想處理異常,在協程內直接使用 try-catch 或者使用 CoroutineExceptionHandler

更多信息能夠閱讀前面提到的這篇 文章

6. 在子協程中使用 CoroutineExceptionHandler

再來一條簡明扼要的:在子協程的構建器中使用 CoroutineExceptionHandler 不會有任何效果。這是由於異常處理是代理給父協程的。由於,你必須在根或者父協程或者 CoroutineScope 中使用 CoroutineExceptionHandler

一樣,更多細節請閱讀 這裏

7. 捕獲 CancellationExceptions

當協程被取消,正在執行的掛起函數會拋出 CancellationException 。這一般會致使協程發生 "異常" 而且當即中止運行。以下面代碼所示:

fun main() = runBlocking {

    val job = launch {
        println("Performing network request in Coroutine")
        delay(1000)
        println("Coroutine still running ... ")
    }

    delay(500)
    job.cancel()
}
複製代碼

500 ms 以後,掛起函數 delay() 拋出了 CancellationException ,協程 "異常結束" 而且中止運行。

>_

Performing network request in Coroutine

Process finished with exit code 0
複製代碼

如今讓咱們假設 delay() 表明一個網絡請求,爲了處理網絡請求可能發生的異常,咱們用 try-catch 代碼塊來捕獲全部異常。

fun main() = runBlocking {

    val job = launch {
        try {
            println("Performing network request in Coroutine")
            delay(1000)
        } catch (e: Exception) {
            println("Handled exception in Coroutine")
        }

        println("Coroutine still running ... ")
    }

    delay(500)
    job.cancel()
}
複製代碼

如今,假設服務端發生了 bug 。catch 分支不只會捕獲錯誤網絡請求的 HttpException ,對於 CancellationExceptions 也是。所以協程不會 「異常中止」,而是繼續運行。

>_

Performing network request in Coroutine
Handled exception in Coroutine
Coroutine still running ... 

Process finished with exit code 0
複製代碼

這可能致使設備資源浪費,甚至在某些狀況下致使崩潰。

要解決這個問題,咱們能夠只捕獲 HttpException

fun main() = runBlocking {

    val job = launch {
        try {
            println("Performing network request in Coroutine")
            delay(1000)
        } catch (e: HttpException) {
            println("Handled exception in Coroutine")
        }

        println("Coroutine still running ... ")
    }

    delay(500)
    job.cancel()
}
複製代碼

或者再次拋出 CancellationExceptions

fun main() = runBlocking {

    val job = launch {
        try {
            println("Performing network request in Coroutine")
            delay(1000)
        } catch (e: Exception) {
            if (e is CancellationException) {
                throw e
            }
            println("Handled exception in Coroutine")
        }

        println("Coroutine still running ... ")
    }

    delay(500)
    job.cancel()
}
複製代碼

以上就是使用 Kotlin Coroutines 最多見的 7 個錯誤。若是你瞭解其餘常見錯誤,歡迎在評論區留言!

另外,不要忘記向其餘開發者分享這篇文章以避免發生這樣的錯誤。Thanks !

Thank you for reading, and have a great day!

相關文章
相關標籤/搜索