原文做者:Lukas Lechnerhtml
原文地址:7 common mistakes you might be making when using Kotlin Coroutinesjava
譯者:秉心說git
在我看來,Kotlin Coroutines(協程) 大大簡化了同步和異步代碼。可是,我發現了許多開發者在使用協程時會犯一些通用性的錯誤。github
有時候你會須要一個 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
中運行這個協程,而後取消協程做用域而不是協程的 job
。markdown
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
複製代碼
有時候你會使用 SupervisorJob
來達到下面的效果:
因爲協程構建器 launch{}
和 async{}
均可以傳遞 Job
做爲入參,因此你能夠考慮向構建器傳遞 SupervisorJob
實例。
launch(SupervisorJob()){
// Coroutine Body
}
複製代碼
可是,就像錯誤 1 ,這樣會打破結構化併發的取消機制。正確的解決方式是使用 supervisorScope{}
做用域函數。
supervisorScope {
launch {
// Coroutine Body
}
}
複製代碼
當你在本身定義的 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()) 都是能夠配合取消的。可是對於你本身的掛起函數,不要忘記考慮取消的狀況。
這一項並不真的是一個 「錯誤」 ,可是仍可能讓你的代碼難以理解,甚至更加低效。一些開發者認爲當調用協程時,就應該切換到後臺調度器,例如,進行網絡請求的 Retrofit 的 suspend
函數,進行數據庫操做的 Room 的 suspend
函數。
這並非必須的。由於全部的掛起函數都應該是主線程安全的,Retrofit 和 Room 都遵循了這一約定。你能夠閱讀個人 這篇文章 以瞭解更多內容。
協程的異常處理很複雜,我花了至關多的時間才徹底理解,並經過 博客 和 講座 向其餘開發者進行了解釋。我還做了一些 圖 來總結這個複雜的話題。
關於 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
。
更多信息能夠閱讀前面提到的這篇 文章 。
再來一條簡明扼要的:在子協程的構建器中使用 CoroutineExceptionHandler
不會有任何效果。這是由於異常處理是代理給父協程的。由於,你必須在根或者父協程或者 CoroutineScope
中使用 CoroutineExceptionHandler
。
一樣,更多細節請閱讀 這裏 。
當協程被取消,正在執行的掛起函數會拋出 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!