kotlin-coroutines (協程),一篇就夠了

Why

  • 簡化異步代碼的編寫。
  • 執行嚴格主線程安全確保你的代碼永遠不會意外阻塞主線程,並加強了代碼的可讀性。
  • 提高代碼安全性,不會發生內存泄露。
  • 協程間通訊。

What

協程的概念在編程語言的早期就出現了,在1967年Simula第一次使用協程。 協程就像很是輕量級的線程。 線程是由系統調度的,線程切換或線程阻塞的開銷都比較大。而協程依賴於線程,可是協程掛起時不須要阻塞線程,幾乎是無代價的,協程是由開發者控制的。因此協程也像用戶態的線程,很是輕量級,一個線程中能夠建立任意個協程。html

舉個你妹子都聽得懂的栗子,不必定很準確。假如要從 地鐵A站地鐵C站 和你妹子約會,可是當到達 地鐵B站 的時候,你想來想去應該去給妹子買個精美的禮物,大概要1個小時,而從 A站C站 只有這一輛列車,只不過開的飛快,每10分鐘又回到 A站從新出發,地鐵比如一條線程,你去買禮物回到B站比如一個任務。在同步阻塞的狀況下,是你去買禮物這段時間,地鐵一直等你,直到你帶着禮物回來。在有協程的狀況下,你去買禮物比如一段協程,地鐵把你在B站放下(掛起),地鐵繼續往前開,你買好禮物了就在B站等下趟地鐵來,上車繼續(恢復)前去和妹子約會。在異步的狀況下是,你去買禮物(異步任務),地鐵繼續往前開,可是地鐵司機給你一個電話號碼(callback),你買禮物回到B站的時候須要打個人電話號碼,才讓你上車。異步callback的時候有個問題,每一個人下車去臨時辦事司機還要給他一個電話號碼,若是他出異常不回來了,可能會致使司機的電話號碼泄露,很是不安全。android

How

在Android開發中,常常遇到的問題:git

  • Long running tasks
  • Main-safety
  • Leak work

Long running tasks

  • 一次CPU循環小於0.0000000004秒
  • 一次網絡請求大約0.4秒

在Android中主線程主要用戶UI的渲染和響應用戶手勢交互,以及輕量級的邏輯運算。若果在主線程發起一個請求,將會致使應用變慢、變卡、沒法響應用戶的交互,很容易形成ARN,用戶體驗極差。因此業界通行的作法是經過callback實現異步回調:github

class ViewModel: ViewModel() {
   fun fetchDocs() {
       get("developer.android.com") { result ->
           show(result)
       }
    }
}
複製代碼

上面callback的示例只是一層回調的狀況,假若有兩個甚至更多的異步請求,並且存在下一個請求依賴上一個請求的結果,就會存在層層嵌套,固然目前比較流行的作法是用Retrofit的轉換函數flatMap實現鏈式調用,可是代碼看起來仍是很臃腫。若是使用協程上面的代碼能夠簡化成這樣:編程

// Dispatchers.Main
suspend fun fetchDocs() {
    // Dispatchers.Main
    val result = get("developer.android.com")
    // Dispatchers.Main
    show(result)
}

suspend fun get(url: String) = withContext(Dispatchers.IO) {
    // Make a request
    // Dispatchers.IO
}
複製代碼

Coroutines提供一個很好途徑能夠簡化耗時任務的代碼編寫,使得異步callback的代碼能夠像同步代碼同樣順序編寫。Coroutines在普通的方法上面加上兩個新的操做。除了callreturn,Coroutines還增長 suspendresume。 協程使用棧幀管理當前運行的方法和方法的全部本地變量。當協程開始掛起,當前棧幀被複制並保存以供後續使用。當協程開始被恢復,棧幀將從它被保存的地方恢復回來,當前棧幀的方法繼續執行。api

  • suspend: 掛起當前協程的執行,將當前執行棧幀的全部本地變量和函數copy出來並保存。
  • resume: 從掛起的地方繼續當前協程的執行。

suspend functions只能在協程或者suspend functions 中被調用。安全

Main-safety with coroutines

在Kotlin協程中,寫的好的suspend functions 老是應該能夠安全的從主線程被調用,也應該容許從任何線程被調用。使用suspend修飾的 function並非告訴Kotlin這個方法在主線程運行。bash

爲了寫一個主線程安全的耗時方法,你可讓協程在Default 或者 IO dispatcher中執行(用withContext(Dispatchers.IO)指定在IO線程中運行)。在協程全部的協程必須運行在dispatcher中,即便他們運行在主線程中。Coroutines將會掛起本身,dispatcher知道如何恢復他們。網絡

爲了指定coroutines在什麼線程運行,kotlin提供了四種Dispatchers:併發

Dispatchers 用途 使用場景
Dispatchers.Main 主線程,和UI交互,執行輕量任務 1.call suspend functions。2. call UI functions。 3. Update LiveData
Dispatchers.IO 用於網絡請求和文件訪問 1. Database。 2.Reading/writing files。3. Networking
Dispatchers.Default CPU密集型任務 1. Sorting a list。 2.Parsing JSON。 3.DiffUtils
Dispatchers.Unconfined 不限制任何制定線程 高級調度器,不該該在常規代碼裏使用

假如你在Room中使用suspend functionsRxJavaLiveData,它自動提供了主線程安全。

// 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
複製代碼

Leak work

你的程序裏面可能會有成千上萬個協程,你很難經過代碼手動追蹤它們,假如你經過代碼手動追蹤他們以確保它們完成或取消,那麼代碼會顯得臃腫且很容易出錯。若是代碼不是很完美,可能會失去對coroutine的追蹤,並致使任務泄露。任務泄露就像內存泄露,可是更嚴重。它不但會浪費內存的使用,還有cpu、磁盤,甚至會發起一個網絡請求。

在android中,咱們知道Activity和Fragment等都是有生命週期的,咱們一般的模式是當前頁面退出的時候,取消全部的異步任務。假若有一個異步的網絡請求,在當前頁面銷燬的時候還在執行,會致使哪些問題:

  • 空指針異常。爲請求結果回來以後去更新UI狀態,而意外致使空指針異常。
  • 浪費內存資源。
  • 浪費CPU資源。

爲了不協程泄露,kotlin引入 結構化併發 。結構化併發是語言特性和最佳實踐的組合,若是咱們遵循最佳實踐,將幫助追蹤運行在協程中的任務。在Android中結構化併發能夠幫咱們作以下三件事:

  • 取消任務,當協程再也不須要的時候。
  • 追蹤任務,當協程運行的時候。
  • 傳播錯誤信號,當協程執行失敗的時候。

解決方式:

  • CoroutineScope 取消任務,實際上是經過關聯的job取消任務。
  • 任務追蹤,coroutines的結構化併發經過coroutineScopesupervisorScope 保證 suspend function的全部任務完成才返回。
  • 傳播錯誤信號coroutineScope 保證錯誤雙向傳遞,只要有一個子coroutine失敗或出現異常,異常往父域傳遞,並取消全部的子coroutines。而 supervisorScope 實現單向錯誤傳遞,適用於做業監控器。
CoroutineScope

在kotlin中全部的協程必須運行在CoroutineScope中,scope幫你追蹤全部協程的狀態,可是它不像Dispatcher,並不運行你的協程。它能夠取消全部在裏面啓動的協程。啓動一個新協程:

scope.launch {
    // This block starts a new coroutine 
    // "in" the scope.
    // 
    // It can call suspend functions
   fetchDocs()
}
複製代碼

建立CoroutineScope的常見方式以下:

  • CoroutineScope(context: CoroutineContext),api方法,如:val scope = CoroutineScope(Dispatchers.Main + Job()),或者以下:
class LifecycleCoroutineScope : CoroutineScope, Closeable {

    private val job = JobSupervisorJob()
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job

    override fun close() {
        job.cancel()
    }
}
複製代碼
class SimpleRetrofitActivity : FragmentActivity() {
    private val activityScope = LifecycleCoroutineScope()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_simple_retrofit)

        // some other code ...
    }

    override fun onDestroy() {
        super.onDestroy()
        activityScope.close()
    }
    // some other code ...
}
複製代碼
  • coroutineScope:api方法,建立新一個子域,並管理域中的全部協程。注意這個方法只有在block中建立的全部子協程所有執行完畢後,纔會退出。
  • supervisorScope:與 coroutineScope的區別是在子協程失敗時,錯誤不會往上傳遞給父域,因此不會影響子協程。

建立協程的常見方式以下:

  • lauch:協程構建器,建立並啓動(也能夠延時啓動)一個協程,返回一個Job,用於監督和取消任務,用於無返回值的場景。
  • async:協程構建器,和launch同樣,區別是返回一個Job的子類 Deferred,惟一的區別是能夠經過await獲取完成時的返回值,或者捕獲異常(異常處理也不同)。

在Android中有一個kotlin的ViewModel的擴展庫 lifecycle-viewmodel-ktx:2.1.0-alpha04,能夠經過viewModelScope擴展屬性啓動協程,viewModelScope綁定了activity的生命週期,activity銷燬的時候會自動取消在這個scope中啓動的全部協程。

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
        }
    }
}
複製代碼

注意,協程的取消是協做的,當協程掛起的時候被取消將會拋一個 CancellationException,即便你捕獲了這個異常,這個協程的狀態也變爲取消狀態。假如你是一個計算協程,而且沒有檢查取消狀態,那麼這個協程不能被取消。

val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
    var nextPrintTime = startTime
    var i = 0
    while (i < 5) { // computation loop, just wastes CPU
        // print a message twice a second
        if (System.currentTimeMillis() >= nextPrintTime) {
            println("job: I'm sleeping ${i++} ...")
            nextPrintTime += 500L
        }
    }
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")
複製代碼
任務追蹤

有時,咱們但願兩個或多個請求同時併發,並等待他們所有完成,suspend function 加上 coroutineScope 建立的子域能夠保證所有子協程完成才返回。

suspend fun fetchTwoDocs() {
    coroutineScope {
        launch { fetchDoc(1) }
        async { fetchDoc(2) }
    }
}
複製代碼
傳播錯誤信號

注意協程的結構化併發是基於語言特性加上最佳實踐的,以下方式會致使,錯誤丟失:

val unrelatedScope = MainScope()
// example of a lost error
suspend fun lostError() {
    // async without structured concurrency
    unrelatedScope.async {
        throw InAsyncNoOneCanHearYou("except")
    }
}
複製代碼

上面代碼丟失錯誤是由於 async的恢復須要調用await,這樣才能將異常從新上傳,而在suspend function 使用了另一個協程域,致使lostError不會等待自做業的完成就退出了。正確的結構化併發:

suspend fun foundError() {
    coroutineScope {
        async { 
            throw StructuredConcurrencyWill("throw")
        }
    }
}
複製代碼

你能夠經過CoroutineScope (注意是大寫開頭的C) 和 GlobalScope來建立 非結構化的協程,僅僅當你認爲它的生命週期比調用者生命週期更長。

總結

  • CoroutineScope:協程做用域包含 CoroutineContext,用於啓動協程,並追蹤子協程,實際上是經過Job追蹤的。
  • CoroutineContext:協程上下文,主要包含JobCoroutineDispatcher,表示一個協程的場景。
  • CoroutineDispatcher:協程調度器,決定協程所在的線程或線程池。它能夠指定協程運行於特定的一個線程、一個線程池或者不指定任何線程。
  • Job:任務,封裝了協程中須要執行的代碼邏輯。Job 能夠取消而且有簡單生命週期,它有三種狀態:isActiveisCompletedisCancelled
  • Deferred:Job的子類,有返回值的Job,經過await獲取。
  • 協程構建器包括:lauchasync

示例

參考

相關文章
相關標籤/搜索