- 原文地址:Advanced Kotlin Coroutines tips and tricks
- 原文做者:Alex Saveau
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:nanjingboy
- 校對者:zx-Zhu
協程從 1.3 開始成爲穩定版!前端
開始 Kotlin 協程很是簡單:只需將一些耗時操做放在 launch
中便可,你作到了,對不?固然,這是針對簡單的狀況。但很快,併發與並行的複雜性會慢慢堆積起來。android
當你深刻研究協程時,如下是一些你須要知道的事情。ios
沒有辦法繞過它:在某些時候,你不得不用原生 Java 流。這裏的問題(不少狀況下 😉)是使用流將會堵塞當前線程。這在協程中是一個壞消息。如今,若是你想要取消一個協程,在可以繼續執行以前,你不得不等待讀寫操做完成。git
做爲一個簡單可重複的例子,讓咱們打開 ServerSocket
而且等待 1 秒的超時鏈接:github
runBlocking(Dispatchers.IO) {
withTimeout(1000) {
val socket = ServerSocket(42)
// 咱們將卡在這裏直到有人接收該鏈接。難道你不想知道爲何嗎?😜
socket.accept()
}
}
複製代碼
應該能夠運行,對嗎?不。後端
如今你的感覺有點像:😖。 那麼咱們如何解決呢?bash
當 Closeable
APIs 構建良好時,它們支持從任何線程關閉流並適當地失敗。併發
注意:一般狀況下,JDK 中的 APIs 遵循了這些最佳實踐,但需注意第三方
Closeable
APIs 可能並無遵循。 你被提醒過了。app
幸好 suspendCancellableCoroutine
函數,當一個協程被取消時咱們能夠關閉任何流:socket
public suspend inline fun <T : Closeable?, R> T.useCancellably(
crossinline block: (T) -> R
): R = suspendCancellableCoroutine { cont ->
cont.invokeOnCancellation { this?.close() }
cont.resume(use(block))
}
複製代碼
確保這適用於你正在使用的 API !
如今阻塞的 accept
調用被 useCancellably
包裹,該協程會在超時觸發的時候失敗。
runBlocking(Dispatchers.IO) {
withTimeout(1000) {
val socket = ServerSocket(42)
// 拋出 `SocketException: socket closed` 異常。好極了!
socket.useCancellably { it.accept() }
}
}
複製代碼
成功!
若是你不支持取消怎麼辦?如下是你須要注意的事項:
onDestroy
中清理資源,這尤爲重要。解決方法: 將協同程序移動到 ViewModel
或其餘上下文無關的類中並訂閱它的處理結果。Dispatchers.IO
來處理阻塞操做,由於這可讓 Kotlin 留出一些線程來進行無限等待。suspendCancellableCoroutine
替換 suspendCoroutine
。launch
vs. async
因爲上面關於這兩個特性的回答已通過時,我想我會再次分析它們的差別。
launch
異常冒泡當一個協程崩潰時,它的父節點將被取消,從而取消全部父節點的子節點。一旦整個樹節點中的協程完成取消操做,異常將會發送到當前上線文的異常處理程序。在 Android 中,這意味着 你的 程序將會 崩潰,而無論你使用什麼來進行調度。
async
持有本身的異常這意味着 await()
顯式處理全部異常,安裝 CoroutineExceptionHandler
將無任何效果。
launch
「blocks」 父做用域雖然該函數會當即返回,但其父做用域將 不會 結束,直到使用 launch
構建的全部協程以某種方式完成。所以若是你只是想等待全部協程完成,在父做用域末尾調用全部子做業的 join()
就沒有必要了。
與你指望的可能不一樣,即便未調用 await()
,外部做用域仍將等待async
協程完成。
async
返回值這一部分至關簡單:若是你須要協程的返回值,async
是惟一的選擇。若是你不須要返回值,使用 launch
來建立反作用。而且在繼續執行以前須要完成這些反作用才須要使用 join()
。
join()
vs. await()
join()
在 await()
時 不會 從新拋出異常。但若是發生錯誤,join()
會取消你的協程,這意味着在 join()
掛起後調用任何代碼都不會起做用。
如今你瞭解了你所使用不一樣構造器異常處理機制的差別,你會陷入兩難境地:你想記錄異常而不崩潰(因此咱們不能使用 launch
),可是你不想手動調用 try
/catch
(因此咱們不能使用 async
)。因此這讓咱們無所適從?謝天謝地。
記錄異常是 CoroutineExceptionHandler
派上用場的地方。但首先,讓咱們花點時間瞭解在協程中拋出異常時究竟發生了什麼:
Continuation
恢復。CancellationException
,那麼將經過當前的 CoroutineContext
請求第一個 CoroutineExceptionHandler
。ServiceLoader
用於定位全局處理程序。考慮到這一點,咱們有如下幾個選擇:
launch
的任何協程崩潰時都會調用它(hacky)。GlobalScope
,或將處理程序添加到你使用的每一個做用域,但這很煩人並使日誌記錄由默認變成了可選。最後一個方案是所推薦的,由於它具備靈活性而且須要最少的代碼和技巧。
對於應用程序範圍內的做業,你將使用帶有日誌記錄處理程序的 AppScope
。對於其餘業務,你能夠在日誌記錄崩潰的適當位置添加處理程序。
val LoggingExceptionHandler = CoroutineExceptionHandler { _, t ->
Crashlytics.logException(t)
}
val AppScope = GlobalScope + LoggingExceptionHandler
複製代碼
class ViewModelBase : ViewModel(), CoroutineScope {
override val coroutineContext = Job() + LoggingExceptionHandler
override fun onCleared() = coroutineContext.cancel()
}
複製代碼
不是很糟糕
任什麼時候候咱們必須處理邊緣狀況,事情每每會很快變得混亂。我但願這篇文章可以幫助你瞭解在非標準條件下可能遇到的各類問題,以及你可使用的解決方案。
Happy Kotlining!
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。