摘要:原文鏈接原文作者今天譯者注年月日是版本的發布日,同時會對協程的結構化并發做一些介紹。進一步的閱讀結構化并發的概念背后有更多的哲學。現代語言開始為我們提供一種以完全非結構化啟動并發任務的方式,這玩意該結束了。
原文鏈接:Structured concurrency
原文作者:Roman Elizarov
今天(譯者注:18年9月12日) 是 kotlinx.coroutines 0.26.0 版本的發布日,同時會對 Kotlin 協程的「結構化并發」做一些介紹。它不僅僅是一個功能改變——它標志著編程風格的巨大改變,我寫這篇文章就是為了解釋這一點。
在 Kotlin 1.1 也就是 2017年初, 首次推出協程作為實驗性質的特性開始,我們一直在努力向程序員解釋協程的概念,他們過去常常使用線程理解并發,所以我們舉的例子和標語是"協程是輕量級線程"。
此外,我們的關鍵 api 被設計為類似于線程 api,以簡化學習曲線。這種類比在小規模例子中很適用,但是它不能幫助解釋協程編程風格的轉變。
當我們學習使用線程編程時,我們被告知線程是昂貴的資源,不應該到處創建它們。一個優雅的程序通常在啟動時創建一個線程池然后使用它們搞些事情。有些環境(尤其是 iOS)甚至說"不贊成使用線程"(即使所有的東西仍然在線程上運行)。它們提供了一個系統內的隨時可用的線程池,其中包含可向其提交代碼的相應隊列。
但是協程的情況不同。它可以非常方便地創建很多你需要的協程,因為它們非常廉價。讓我們看一下協程的幾個用例。
異步操作(Asynchronous operations)假設你正在寫一個前端 UI 應用(移動端、web 端或桌面端——對于這個例子并不重要),并且需要向后端發送一個請求,以獲取一些數據并使用結果更新 UI 模型。我們最初推薦這樣寫:
fun requestSomeData() {
launch(UI) {
updateUI(performRequest())
}
}
這里,我們使用 launch(UI) 在 UI 上下文中啟動一個新的協程,調用performRequest 掛起函數對后端執行異步調用,而不阻塞主 UI 線程,然后使用結果更新 UI。每個 requestSomeData 調用都創建自己的協程,這很好,不是嗎?它和 C# JS 和 GO 中的異步編程并沒有太大的不同。
但是這里有個問題。如果網絡或后端出現問題,這些異步操作可能需要很長時間才能完成。此外,這些操作通常在一些 UI 元素(比如窗口或頁面)的范圍內執行。如果一個操作花費的時間太長,通常用戶會關閉相應的 UI 元素并執行其他操作,或者更糟糕的是,重新打開這個 UI 并一次又一次地嘗試該操作。但是前面的操作仍然在后臺運行,當用戶關閉相應的 UI 元素時,我們需要某種機制來取消它。在 Kotlin 協程中,這導致我們推薦了一些非常棘手的設計模式,人們必須在代碼中遵循這些模式,以確保正確處理這種取消。此外,你必須是中記住指定適當的上下文,否則 updateUI 可能會被錯誤的線程調用,從而破壞 UI。這是容易出錯的。一個簡單的launch{ ... } 很容易寫出來,但是你不應該寫成這樣。
在更哲學的層面上,很少像線程那樣"全局"地啟動協程。線程總是與應用程序中的某個局部作用域相關,這個局部作用域是一個生命周期有限的實體,比如 UI 元素。因此,對于結構化并發,我們現在要求在一個協程作用域中調用 launch,協程作用域是由你的生命周期有限的對象(如 UI 元素或它們相應的視圖模型)實現的接口。你實現一次協程作用域后, 你會發現,在你的 UI 類中有一個簡單的 launch{ … } ,然后你寫很多遍,變得極容易寫又正確:
fun requestSomeData() {
launch {
updateUI(performRequest())
}
}
注意,協程作用域的實現還為 UI 更新定義了適當的協程上下文。你可以在其文檔頁面上找到一個完整的協程作用域實現示例。對于一些比較少見的情況,你需要一個全局協程,它的生命周期受整個應用生命周期限制,我們現在提供了 GlobalScope (全局作用域)對象,因此以前全局協程的launch{ … } 變成了 GlobalScope.launch { … } ,協程的"全局"含義變得直觀了。
并行分解(Parallel decomposition)我已經就 Kotlin 協程進行了多次 討論,,下面的示例代碼展示了如何并行加載兩個圖片并在稍后將它們組合起來——這是一個使用 Kotlin 協程并行分解工作的慣用示例:
suspend fun loadAndCombine(name1: String, name2: String): Image {
val deferred1 = async { loadImage(name1) }
val deferred2 = async { loadImage(name2) }
return combineImages(deferred1.await(), deferred2.await())
}
不幸的是,這個例子在很多層面上都是錯誤的。掛起函數loadAndCombine 本身將從一個已經啟動的執行更大操作的協程內部調用。如果這個操作被取消了呢?然后加載這兩個圖片仍然沒有收到影響。這不是我們想從可靠代碼中的得到的,特別是如果這些代碼是許多客戶端使用后端服務的一部分。
我們推薦的解決方案是寫成這樣async(conroutineContext){ … } ,以便在子協程中加載兩個圖片,當父協程被取消時,子協程將被取消。
它仍然不完美。如果加載第一個圖片失敗,那么 deferred1.await() 將拋出相應的異常,但是加載第二個圖片的第二個 async 協程仍然在后臺工作。解決這個問題就更復雜了。
我們在第二個用例中看到了同樣的問題。一個簡單的 async { … } 很容易寫,但是你不應該寫成這樣。
使用結構化并發,async 協程構建器就像 luanch 一樣,變成了協程作用域上的一個擴展。你不能再簡單的編寫 async{ … } ,你必須提供一個作用域。并行分解的一個恰當的例子是:
suspend fun loadAndCombine(name1: String, name2: String): Image =
coroutineScope {
val deferred1 = async { loadImage(name1) }
val deferred2 = async { loadImage(name2) }
combineImages(deferred1.await(), deferred2.await())
}
你必須將代碼封裝到 coroutineScope { ... } 塊中,這個塊為你的操作及其范圍建立了邊界。所有異步協程都成為這個范圍的子協程,如果該作用域因為異常導致失敗或被取消了,它所有的子協程也將被取消。
進一步的閱讀結構化并發的概念背后有更多的哲學。我強烈推薦閱讀 “結構化并發的注意事項,或:Go 語句的危害” ,它很好的對比了經典的 goto-statement 和結構化編程。
現代語言開始為我們提供一種以完全非結構化啟動并發任務的方式,這玩意該結束了。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/7361.html
摘要:讓我們探討一下如何確保你的工作脫離主線程運行并保證執行。這確保在默認情況下,你的工作是同步運行的,并且在主線程之外運行。這是應該脫離主線程運行的工作,但是,因為它與直接相關,所以如果關閉應用程序則不需要繼續。 原文地址:WorkManager Basics 原文作者:Lyla Fujiwara 譯文出自:掘金翻譯計劃 本文永久鏈接:github.com/xitu/gold-m… 譯者:Ri...
閱讀 2034·2021-11-11 16:54
閱讀 2111·2019-08-30 15:55
閱讀 3611·2019-08-30 15:54
閱讀 391·2019-08-30 15:44
閱讀 2228·2019-08-30 10:58
閱讀 424·2019-08-26 10:30
閱讀 3048·2019-08-23 14:46
閱讀 3191·2019-08-23 13:46