原文連接 Kotlin 協程使用手冊。javascript
最近抽出閒暇,把 kotlinx.coroutines 官方的三份入手指南翻譯了一下,掛在了 GitBook ,能夠直接去這裏查看。不過,文檔的內容其實仍是比較多的,爲了釐清協程的特殊之處,下面我就總結一番。html
協程的定義其實不太好描述,那我乾脆由用途及定義,簡述一下協程。前端
標題的說法可能不太準確,但也能一窺其功用。協程是工做在線程之上的。咱們知道線程是由系統(語言系統或者操做系統)進行調度的,切換時有着必定的開銷。而協程,它的切換由程序本身來控制,不管是 CPU 的消耗仍是內存的消耗都大大下降。java
從這一點出發,它的應用場景可能就在於提升硬件性能的瓶頸。譬如說,你啓動十萬個協程不會有什麼問題,但你啓動十萬個線程試試?react
相較於第一點,這纔是協程的本質;同時也是由這一點,協程發揮了很大的做用。在協程中,某段代碼能夠暫停,轉而去執行另外的協程代碼;被暫停的代碼也能夠在你的控制下隨時恢復運行。git
這在前端編程中有一個很大的用處——避免回調地獄。就 Android 編程而言,在 Rx 以前,要獲取某個異步操做的返回結果,標準作法就是定義接口,用回調來接收結果。而 Rx 出現以後,以其巧妙的轉換,經過響應式的代碼,以一層的回調(輔以 lambda 表達式,看起來就像沒有回調同樣)鏈解決了回調地獄的問題。但在這裏,習慣以命令式寫法寫代碼的同窗就須要稍稍理解一些函數式的編程思惟了。協程不同,它的代碼是能夠暫停的!也就是說,在我經過 getUser()
方法異步獲取數據的時候,調用它的代碼塊就能夠選擇掛起,等到獲取到數據,再恢復運行。代碼看起來就這樣:github
val user = getUser() // 這兒的 getUser 就是 suspend function
複製代碼
是否是和同步代碼看起來同樣?編程
寫過 JS 的同窗可能就覺着很眼熟了:架構
async function getUser() {
try {
const response = await fetchUser();
// ...
} catch (e) {
// error handle
}
}
複製代碼
沒錯,經過協程,Kotlin 是能夠寫出相似代碼來的!異步
首先,須要經過構造器來啓動協程。官方目前提供的基礎構造器有兩個:
launch
runBlocking
它們都會啓動一個協程,區別在於前者不會阻塞當前線程,而且會返回一個協程的引用,然後者會等待協程的代碼執行結束,再執行剩下的代碼。
其次,關於協程,Kotlin 新增了一個關鍵字:suspend
,被該關鍵字修飾的函數/方法/代碼塊只能由協程代碼(也就是上述構造器的代碼塊參數內部)或者被 suspend
修飾的函數/方法/代碼塊調用。說簡單一點,suspend fun
只能被 suspend fun
調用(協程構造器的最後一個參數的類型聲明就是 suspend CoroutineScope.() -> Unit
)。
知道了這兩點,就能夠寫出最簡單的協程代碼:
fun main(args: Array<String>) {
repeat(100_000) { // 啓動十萬個協程試試
launch { suspendPrint() }
}
Thread.sleep(1200) // 等待協程代碼的結束
}
suspend fun suspendPrint() {
delay(1000)
println(".")
}
複製代碼
其中的 delay
就是一個 suspend fun
。
除了以上兩點,另外一個很重要的概念就是上下文(context
)。協程雖然是依賴於線程的,但一個協程並不是就綁死在一個線程上。啓動協程的時候能夠指定上下文,在協程內部也能夠經過 withContext
切換上下文。而這個上下文,也就是一個 CoroutineDispatcher
類的對象,從名字能夠看出,就是由它去進行協程調度。好比,若是你須要新建一個線程去跑協程的代碼,能夠這樣:
launch(context = newSingleThreadContext("new-thread")) { delay(1000) }
複製代碼
以上三點是我我的認爲重要的內容,固然還有協程的取消、協程的生命週期、協程與子協程的關係等等,這些要點能夠去官方文檔或者個人翻譯查看,內容寫得很棒。
就我我的所知,async
與 await
做爲 JS 與 C# 的兩個關鍵字,精簡了異步操做(固然,這兩門語言的細節並不同)。可是在 Kotlin 中,async 實際上是一個普通的函數:
fun main(args: Array<String>) = runBlocking<Unit> {
val result: Deferred<String> = async { doSomethingTimeout() }
println("I will got the result ${result.await()}")
}
suspend fun doSomethingTimeout(): String {
delay(1000)
return "Result"
}
複製代碼
在這裏, async
代碼塊會新啓動一個協程後當即執行,而且返回一個 Deferred
類型的值,調用它的 await
方法後會暫停當前協程,直到獲取到 async
代碼塊執行結果,當前協程纔會繼續執行。
其實談到這個,就不得不提一下 Retrofit 了,做爲 RESTful 架構的優秀解決方案,有人已經爲其適配了協程版的 adapter 了。我知道的有兩個:
其實前者並非 Retrofit 的 Adapter,Andrey Mischenko 只是爲 Call
類添加了擴展函數而已。可是它們都是使用 Deferred
對象來處理結果。
這兒有個 channel 的概念,顧名思義,它的做用就在於收發事件。調用它的 send
與 receive
方法,就是最簡單的使用了。不過要注意,這兩個方法會互相等待,因此它們確定得運行在不一樣的協程。
fun main(args: Array<String>) = runBlocking<Unit> {
val channel = Channel()
launch {
for (x in 1..5) channel.send(x)
channel.close()
}
for (x in 1..5) println(channel.receive())
// or `for (x in channel) println(x)`
}
複製代碼
如上所示,channel 其實自己就能夠迭代,迭代的結束條件就是 channel.close()
。
onClick
方法官方文檔提供了一個 channel 版的 onClick
方法的實現,我以爲比較好用:
fun View.onClick(action: suspend (View) -> Unit) {
val eventActor = actor<View>(UI) {
for (event in channel) action(event)
}
setOnClickListener {
eventActor.offer(it)
}
}
複製代碼
這裏的 actor
內部有一個 channel 用於接收外部的數據,點擊事件產生的時候,經過 actor
向其發送數據,channel 迭代就會向前移動,調用傳入的 action
。這裏還能夠經過參數處理背壓的問題。
從這個應用能夠延展開來,凡是由事件觸發的操做,均可以用相似的思路來實現。固然,不管實現方式的好與壞。
截至目前來看,協程與 Rx 彷佛不能共存,它們的功用大多重複,致使許多場景非此即彼。不過經過官方的第三份手冊,我才發現協程還專門爲 Rx 寫了一個模塊,讓咱們可以以協程的方式寫 Rx 代碼。須要介紹的是 publish
函數,他就是二者的橋樑:
fun range(context: CoroutineContext, start: Int, count: Int): Publisher<Int> =
publish<Int>(context) {
for (x in start until start + count) send(x)
}
複製代碼
publish
內部能夠用 channel 的方式去組織代碼,經過 send
方法將數據流向下一級,它返回的 Publisher
就是 Rx 標準中的那個,能夠經過擴展方法 consumeEach
來接收每一項數據。
range(CommonPool, 1, 5).consumeEach { println(it) }
複製代碼
先後幾天時間,翻譯了三篇指南,切身體會到看一遍與寫一遍的差距。這篇文章旨在羅列要點,許多細節並未說明,更詳盡的內容仍是須要文檔。固然,也能夠加入 kotlinlang 的 coroutine channel 參與討論。