本文翻譯自 Sean McQuillan 的 Kotlin coroutines 入門系列。看了他的三篇文章,真正瞭解了協程出現的意義,它能幫開發者解決的問題,並掌握了它的基本用法。 原文地址:html
對於 Android 開發者來講,咱們能夠將協程運用在如下兩個場景:android
suspend
函數來執行一些操做而不阻塞主線程。咱們都知道不管是請求網絡仍是讀取數據庫都是耗時任務,咱們不能在主線程去執行這些耗時操做。如今的手機 CPU 頻率都是很高的,Pixel 2 的單核 CPU 週期小於 0.0000000004 秒(0.4納秒),而一次網絡請求大約是 0.4 秒(400 毫秒)。能夠這麼說,一眨眼功夫能夠完成一次網絡請求,但同時 CPU 已經執行了 10 億屢次。 Android 平臺,主線程是 UI 線程,主要負責 View 的繪製(16 ms)和響應用戶操做。若是咱們在主線程作耗時操做,就會阻塞主線程,形成 View 不能及時刷新,不能及時響應用戶操做,從而影響用戶體驗。 爲了解決以上問題,咱們通常使用 Callbacks 的方式。舉個例子:git
class ViewModel: ViewModel() {
fun fetchDocs() {
get("developer.android.com") { result ->
show(result)
}
}
}
複製代碼
儘管 get()
函數是被主線程調用的,但它的實現確定是要在其餘線程完成網絡請求的。當結果返回時,Callback 又會在主線程被調用,來將結果顯示到 UI 上。github
協程能夠簡化異步代碼,用協程咱們能夠更方便地重寫上面的例子:數據庫
// Dispatchers.Main
suspend fun fetchDocs() {
// Dispatchers.IO
val result = get("developer.android.com")
// Dispatchers.Main
show(result)
}
// look at this in the next section
suspend fun get(url: String) = withContext(Dispatchers.IO){/*...*/}
複製代碼
與普通函數相比,協程添加了 suspend
和 resume
兩種操做。這兩個操做一塊兒完成了 Callback 的工做,但更優雅,就像是用同步代碼完成了異步操做。編程
suspend
:掛起當前協程,保存全部的本地變量;resume
:恢復已經掛起的協程,從它暫停的地方繼續執行。
suspend
是 Kotlin 的一個關鍵字。被suspend
標記的函數,只能在suspend
函數內被調用。咱們可使用協程提供的launch
和async
從主線程啓動一個協程來執行suspend
函數。安全
public fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
public fun <T> CoroutineScope.async( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> T
): Deferred<T> {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyDeferredCoroutine(newContext, block) else
DeferredCoroutine<T>(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
複製代碼
如動畫所示,get()
函數在執行前會掛起(suspend)當前的協程,它內部依舊是經過 IO 線程(Dispatchers.IO
)來執行網絡請求。當請求完成時,它不是經過 Callback 的方式,而是恢復(resume)已經掛起的協程繼續執行 show(result)
函數。任何一個協程被掛起時,當前的棧信息都會被複制並保存,以便在恢復時使用。當全部的協程都被掛起時,主線程不會被阻塞,仍然能夠更新 UI 和響應用戶操做。因而可知,協程爲咱們提供了一種異步操做的簡單實現方式。網絡
使用 Kotlin 協程的一個原則是:咱們應該保證咱們寫的 suspend
函數是主線程安全的,也就是能夠在任何線程中調用它,而不用去讓調用者手動切換線程。 須要注意的是:suspend
函數通常是運行在主線程中的,suspend
不是意味着運行的子線程。也就是說,咱們須要在 suspend
內部指定該函數執行的線程,如不指定,它默認運行在調用者的線程。 若是不是執行耗時任務,咱們可使用 Dispatchers.Main.immediate 來啓動一個協程,下一次 UI 刷新時就會將結果顯示到 UI 上。 全部的 Kotlin協程都必須運行在一個 Dispatcher
中,它提供瞭如下幾種 Dispatcher
來運行協程。併發
Dispatchers | 用途 | 使用場景 |
---|---|---|
Dispatchers.Main | 主線程、UI交互、執行輕量任務 | Call suspend functions, Call UI functions, Update LiveData |
Dispatchers.IO | 網絡請求、文件訪問 | Database, Reading/writing files, Networking |
Dispatchers.Default | CPU密集型任務 | Sorting a list, Parsing JSON, DiffUtils |
Dispatchers.Unconfined | 不限制任何指定線程 | 限制恢復後的線程 |
完整的 get()
函數以下所示:app
// Dispatchers.Main
suspend fun fetchDocs() {
// Dispatchers.Main
val result = get("developer.android.com")
// Dispatchers.Main
show(result)
}
// Dispatchers.Main
suspend fun get(url: String) =
// Dispatchers.IO
withContext(Dispatchers.IO) {
// Dispatchers.IO
/* perform blocking network IO here */
}
// Dispatchers.Main
複製代碼
使用協程咱們能夠自由控制代碼運行的線程,withContext()
爲咱們提供了相似編寫同步代碼的方式來實現異步編程。如上面提到的:咱們應儘可能使用 withContext
確保每一個函數都是線程安全的,不要讓調用者關心要在哪一個線程才能調用該函數。 上面的例子中,fetchDocs
運行在主線程,而 get()
運行在子線程,因爲協程的 掛起/恢復 機制,當 withContext
返回時,當前協程會恢復執行。 在性能方面,withContext
跟 Callbacks 或 RxJava 不相上下。因此咱們不用擔憂性能問題,相信官方也會持續優化。
協程相比於線程來講,它是很輕量的。咱們能夠啓動上百上千個協程,但沒法啓動這麼多的線程。雖然協程很輕量,但它們的實際進行的任務多是耗時的,好比用於讀取數據庫、請求網絡或讀寫文件的協程。所以,咱們仍須要維護好這些協程的完成和取消,不然可能發生任務泄露,這比內存泄漏更嚴重,由於任務可能浪費 CPU、磁盤或網絡資源。 手動管理成百上千個協程是很困難的,爲了不協程泄露,Kotlin 提供了 結構化併發 來幫助咱們更方便地追蹤全部運行中的協程。在 Android 開發過程當中,咱們能夠用它來完成如下三件事:
Kotlin 協程必須運行在 CoroutineScope
中,CoroutineScope
能夠追蹤全部運行中和已掛起的協程,不像上文提到的 Dispatchers
,它只是保證對全部的協程的追蹤,而不會真正地執行它們。所以爲了確保全部的協程都能被追蹤到,咱們不能在 CoroutineScope
外啓動一個新的協程。同時咱們可使用 CoroutineScope
來取消在它內部啓動的全部協程。 咱們須要在普通函數中啓動一個協程,才能調用 suspend
函數。協程提供了兩種方式來啓動一個新的協程。
大多數狀況下,咱們使用 launch
來啓動一個新的協程。launch
函數就像鏈接普通函數和協程的橋樑。
scope.launch {
// This block starts a new coroutine
// "in" the scope.
//
// It can call suspend functions
fetchDocs()
}
複製代碼
launch
和 async
最大的不一樣就是它們處理異常的方式:launch
啓動的協程在發生異常時會馬上拋出,並馬上取消全部協程;而 async
啓動的協程,只有咱們調用 await()
函數時才能獲得內部的異常,若無異常會返回執行結果。
AndroidX Lifecycle KTX 爲咱們提供了 viewModelScope
來方便地在 ViewModel
中啓動協程,並保持對它們的追蹤。
class MyViewModel(): ViewModel() {
fun userNeedsDocs() {
// Start a new coroutine in a ViewModel
viewModelScope.launch {
fetchDocs()
}
}
}
複製代碼
咱們能夠在一個 CoroutineScope
中包含若干個 CoroutineScope
,若是咱們在一個協程中啓動了另外一個協程,其實它們最終都同屬於一個最頂層的 CoroutineScope
,也就是說咱們能夠經過取消最外層的協程來取消全部內部的協程。 若是咱們取消一個已經掛起的協程,它會拋出一個異常 CancellationException
。若是咱們捕獲並消費了這個異常,或者取消一個未掛起的協程,該協程會處於一個 半取消(semi-canceled)狀態。 viewModelScope
啓動的協程會在 ViewModel 銷燬(clear)時自動取消,因此即便咱們其內部執行是一個死循環,也會被自動取消。
fun runForever() {
// start a new coroutine in the ViewModel
viewModelScope.launch {
// cancelled when the ViewModel is cleared
while(true) {
delay(1_000)
// do something every second
}
}
}
複製代碼
咱們可使用協程進行網絡請求、讀寫數據庫等耗時操做。但有時咱們可能須要在一個協程中同時進行兩個網絡請求,這時咱們須要再啓動兩個協程來共同工做。咱們能夠在任何一個 suspend
函數中使用 coroutineScope
或 supervisorScope
來啓動更多的協程。 在一個協程中啓動新的協程可能會形成潛在的任務泄露,由於調用者可能不知道咱們內部的實現。好消息是,結構化併發能夠保證:若是一個 suspend
函數返回了,那麼它內部的全部代碼都已經執行完畢。 這仍然是同步調用的影子。 舉個例子:
suspend fun fetchTwoDocs() {
coroutineScope {
launch { fetchDoc(1) }
async { fetchDoc(2) }
}
}
複製代碼
如上所示:fetchTwoDocs()
內部經過 coroutineScope
又啓動了兩個協程來同時加載兩個文檔,一個方式是 launch
,一種方式是 async
。爲了不 fetchTwoDocs()
任務泄露,coroutineScope
會一直保持掛起狀態,直到內部的全部協程都執行完畢,這時 fetchTwoDocs()
函數纔會返回。
以上示例,咱們同時啓動了 1000 個協程來請求網絡,loadLots()
內部的 coroutineScope
是 該函數調用者的 CoroutineScope 的子集,內部的 coroutineScope
會一直保持這 1000 個協程的追蹤,只有當全部協程都執行完畢,loadLots()
函數纔會返回。
coroutineScope
和 supervisorScope
可讓咱們在任意 suspend
函數內安全啓動協程,直到內部的全部協程都執行完畢,它們纔會返回。此外,若是咱們取消了外層的 scope,內部的子協程也會被取消。 coroutineScope
和 supervisorScope
的區別是:只要 coroutineScope
內的任一協程執行失敗,整個 scope 都會被取消,內部的其餘子協程也會馬上被取消;而 supervisorScope
內的某一協程失敗,不會取消其餘的子協程。
和普通函數同樣,協程在執行失敗時也會拋出異常。suspend
函數內拋出的異常是會向上傳遞的,咱們也可使用 try/catch
語法或其餘方式捕獲異常。可是下面這種異常可能會丟失:
val unrelatedScope = MainScope()
// example of a lost error
suspend fun lostError() {
// async without structured concurrency
unrelatedScope.async {
throw InAsyncNoOneCanHearYou("except")
}
}
複製代碼
以上代碼中,咱們在一個不相關的限定範圍內啓動了一個協程,它並非結構化併發的。因爲 async
函數啓動的協程只有在調用 await()
時纔會拋出異常,因此這個異常可能會丟失,它會被一直保存着直到咱們調用 await()
。 結構化併發能夠保證當一個協程發生異常時,它的調用者或 scope 能夠收到這個異常。 上面代碼用結構化併發的方式改寫以下:
suspend fun foundError() {
coroutineScope {
async {
throw StructuredConcurrencyWill("throw")
}
}
}
複製代碼
總結一下:
coroutineScope
和 supervisorScope
是結構化併發的,能夠追蹤內部的全部協程,包括異常處理、任務取消等。GlobalScope
不是結構化併發的,它是一個全局的 scope,跟 Application 同生命週期。Kotlin Coroutines 與 RxJava&RxAndroid 均可以方便的幫咱們進行異步編程,我的以爲它們在異步編程最大的區別是:Coroutines 的編寫方式更像是同步調用,而 RxJava 是流式編程。但本質上,它們內部都是經過線程池來處理耗時任務。RxJava 的有不少個操做符能夠輔助實現各式各樣的需求,並能保證鏈式調用;Coroutines 是與 Kotlin 結合的最好異步編程方式,目前也有不少的官方支持,相信未來 Coroutines 會有很好的使用體驗和執行性能。
我是 xiaobailong24,您能夠經過如下平臺找到我: