目錄
一、異步編程種類簡介
二、線程
三、回調
四、Future、 Promise 及其他
五、反應式擴展
六、協程
一、異步編程種類簡介
幾十年以來,作為開發人員,我們面臨著需要解決的問題——如何防止我們的應用程序被阻塞。 當我們正在開發桌面應用,移動應用,甚至服務器端應用程序時,我們希望避免讓用戶等待或導致更糟糕的原因成為阻礙應用程序擴展的瓶頸。
有很多途徑來解決這種問題,包括:
- 線程
- 回調
- Future、 Promise 及其他
- 反應式擴展
- 協程
二、線程
到目前為止,線程可能是最常見的避免應用程序阻塞的方法。
fun postItem(item: Item) {val token = preparePost()val post = submitPost(token, item)processPost(post)
}fun preparePost(): Token {// 發起請求并因此阻塞了主線程return token
}
讓我們假設在上面的代碼中,preparePost
?是一個長時間運行的進程,因此會阻塞用戶界面。我們可以做的是在一個單獨的線程中啟動它。這樣就可以允許我們避免阻塞 UI。這是一種非常常見的技術,但有一系列缺點:
- 線程并非廉價的。線程需要昂貴的上下文切換。
- 線程不是無限的。可被啟動的線程數受底層操作系統的限制。在服務器端應用程序中,這可能會導致嚴重的瓶頸。
- 線程并不總是可用。在一些平臺中,比如 JavaScript 甚至不支持線程。
- 線程不容易使用。調試線程與避免競爭條件是我們在多線程編程中遇到的常見問題。
三、回調
使用回調,其想法是將一個函數作為參數傳遞給另一個函數,并在處理完成后調用此函數。
fun postItem(item: Item) {preparePostAsync { token -> submitPostAsync(token, item) { post -> processPost(post)}}
}fun preparePostAsync(callback: (Token) -> Unit) {// 發起請求并立即返回// 設置稍后調用的回調
}
原則上這感覺就像一個更優雅的解決方案,但又有幾個問題:
- 回調嵌套的難度。通常被用作回調的函數,經常最終需要自己的回調。這導致了一系列回調嵌套并導致出現難以理解的代碼。該模式通常被稱為標題圣誕樹(大括號代表樹的分支)。
- 錯誤處理很復雜。嵌套模型使錯誤處理和傳播變得更加復雜。
回調在諸如 JavaScript 之類的事件循環體系結構中非常常見,但即使在那里,通常人們已經轉而使用其他方法,例如 promises 或反應式擴展。
四、Future、 Promise 及其他
futures 或 promises 背后的想法(這也可能會根據語言/平臺而有不同的術語),是當我們發起調用的時候,我們承諾在某些時候它將返回一個名為 Promise 的可被操作的對象。
fun postItem(item: Item) {preparePostAsync() .thenCompose { token -> submitPostAsync(token, item)}.thenAccept { post -> processPost(post)}}fun preparePostAsync(): Promise<Token> {// 發起請求并當稍后的請求完成時返回一個 promisereturn promise
}
這種方法需要對我們的編程方式進行一系列更改,尤其是:
- 不同的編程模型。與回調類似,編程模型從自上而下的命令式方法轉變為具有鏈式調用的組合模型。傳統的編程結構例如循環,異常處理,等等。通常在此模型中不再有效。
- 不同的 API。通常這需要學習完整的新 API 諸如?
thenCompose
?或?thenAccept
,這也可能因平臺而異。 - 具體的返回值類型。返回類型遠離我們需要的實際數據,而是返回一個必須被內省的新類型“Promise”。
- 異常處理會很復雜。錯誤的傳播和鏈接并不總是直截了當的。
五、反應式擴展
Erik Meijer) 將反應式擴展(Rx)引入了 C#. 雖然它在 .NET 平臺上是毫無疑義的, 但是在 Netflix 將它移植到 Java 并取名為 RxJava 之前絕對不是主流。從那時起,反應式被移植到各種平臺,包括 JavaScript(RxJS)。
Rx 背后的想法是走向所謂的“可觀察流”,我們現在將數據視為流(無限量的數據),并且可以觀察到這些流。 實際上,Rx 很簡單,?Observer Pattern?帶有一系列擴展,允許我們對數據進行操作。
在方法上它與 Futures 非常相似,但是人們可以將 Future 視為一個離散元素,而 Rx 返回一個流。然而,與前面類似,它還介紹了一種全新的思考我們的編程模型的方式,著名的表述是:
“一切都是流,并且它是可被觀察的”
這意味著處理問題的方式不同,并且在編寫同步代碼時從我們使用的方式發生了相當大的轉變。與 Futures 相反的一個好處是,它被移植到這么多平臺,通常我們可以找到一致的 API 體驗,無論我們使用 C#、Java、JavaScript,還是 Rx 可用的任何其他語言。
此外,Rx 確實引入了一種更好的錯誤處理方法。
六、協程
Kotlin 編寫異步代碼的方式是使用協程,這是一種計算可被掛起的想法。即一種函數可以在某個時刻暫停執行并稍后恢復的想法。
協程的一個好處是,當涉及到開發人員時,編寫非阻塞代碼與編寫阻塞代碼基本相同。編程模型本身并沒有真正改變。
以下面的代碼為例:
fun postItem(item: Item) {launch {val token = preparePost()val post = submitPost(token, item)processPost(post)}
}suspend fun preparePost(): Token {// 發起請求并掛起該協程return suspendCoroutine { /* ... */ }
}
此代碼將啟動長時間運行的操作,而不會阻塞主線程。preparePost
?就是所謂的?可掛起的函數
,因此它含有?suspend
?前綴。這意味著如上所述,該函數將被執行、暫停執行以及在某個時間點恢復。
- 該函數的簽名保持完全相同。唯一的不同是它被添加了?
suspend
?修飾符。但是返回類型依然是我們想要的類型。 - 編寫這段代碼代碼就好像我們正在編寫同步代碼,自上而下,不需要任何特殊語法,除了使用一個名為?
launch
?的函數,它實質上啟動了該協程(在其他教程中介紹)。 - 編程模型和 API 保持不變。我們可以繼續使用循環,異常處理等,而且不需要學習一整套新的 API。
- 它與平臺無關。無論我們是面向 JVM,JavaScript 還是其他任何平臺,我們編寫的代碼都是相同的。編譯器負責將其適應每個平臺。
協程并不是一個新的概念,它并不是 Kotlin 發明的。它們已經存在了幾十年,并且在 Go 等其他一些編程語言中很受歡迎。但重要的是要注意就是他們在 Kotlin 中實現的方式,大部分功能都委托給了庫。事實上,除了?suspend
?關鍵字,沒有任何其他關鍵字被添加到語言中。這也是與其他語言的不同之處,例如 C# 將?async
?以及?await
?作為語法的一部分。而在 Kotlin 中,他們都只是庫函數。