協(xié)程 Kotlin Coroutine 初探

協(xié)程 kotlin Coroutine

目錄:

1. Coroutine 的基本使用

1.1 小結(jié)

2. CoroutineScope 類 和 coroutineScope(xxx) 方法
  • 2.1 CoroutineScope 使用的代碼示例
    - 2.1.1 在 Activity 中的使用
    - 2.1.2 在 ViewModel 中使用以及為什么要在 ViewModel 中使用
  • 2.2 ViewModel 自動銷毀 CoroutineScope 的邏輯
  • 2.3 withContext(xxx) 用作切換線程
  • 2.4 小結(jié)
3. launch -> 創(chuàng)建協(xié)程
  • 3.1 launch() 的參數(shù)和返回結(jié)果說明
  • 3.2 什么是 Job
  • 3.3 CoroutineScope.async() 方法
  • 3.4 小結(jié)
4. suspend 是什么逊脯,「掛起」作用是什么
  • 4.1 「掛起函數(shù)」的使用和代碼運行分析
    • 4.1.1 同一線程中代碼運行邏輯
    • 4.1.2 在當(dāng)前線程中新建一個線程的代碼運行邏輯--未使用 suspend
    • 4.1.3 使用了 suspend 標(biāo)注戒突, 代碼的運行邏輯
  • 4.2 「非阻塞掛起」的含義
  • 4.3 完整測試代碼以及執(zhí)行結(jié)果
  • 4.4 suspend b() 運行時的線程切換
  • 4.5 插入一個小點:調(diào)度器和線程池
  • 4.6 「掛起函數(shù)」小結(jié)
5. 調(diào)度器 CoroutineDispatcher
  • 5.1 CoroutineDispatcher 的種類
6. 說一說協(xié)程中常見的類
  • 6.1 CoroutineContext 的繼承關(guān)系
  • 6.2 Coroutine 的繼承關(guān)系
7. 總結(jié)

正文

想著把協(xié)程說清楚的目的,能不能說清楚,看看下面行不行拍皮。

coroutines 協(xié)程從 kotlin 1.3 開始發(fā)布正式版兆旬,不在是實驗階段了译秦。
修改地址 1.3 changeLog
github 地址: kotlinx.coroutines

目前協(xié)程已經(jīng)支持了多平臺两残,在 Android 中使用需要添加依賴:

api 引入.png

先把協(xié)程中的部分類的繼承關(guān)系梳理一下,這里先簡單的用一張類繼承圖表示委刘,詳細(xì)的一些類的介紹丧没,會在下面的內(nèi)容逐漸涉及到。

常見類繼承圖

1. Coroutine 的基本使用

官方示例代碼如下:

suspend fun main() = coroutineScope {
    launch { 
       delay(1000)
       println("Kotlin Coroutines World!") 
    }
    println("Hello")
}

代碼運行結(jié)果如下:

Hello
Kotlin Coroutines World!

從運行結(jié)果來看锡移,launch{} 中的代碼應(yīng)該和外面的代碼不再同一個線程呕童,下面我們驗證一下。

我們把代碼稍微修改一下淆珊,再次運行一下:

suspend fun mainTest() {
    coroutineScope {
        println("11111 線程 是" + Thread.currentThread())
        launch {
            println("22222 線程 是" + Thread.currentThread())
            delay(1000)
            println("Kotlin Coroutines World!")
        }
        println("33333 線程 是" + Thread.currentThread())
    }
}

這是代碼運行結(jié)果為:

11111 線程 是Thread[main,5,main]
33333 線程 是Thread[main,5,main]
22222 線程 是Thread[DefaultDispatcher-worker-1 @coroutine#1,5,main]
Kotlin Coroutines World!

可參考鏈接:https://play.kotlinlang.org/#eyJ2ZXJzaW9uIjoiMS4zLjMwIiwiY29kZSI6ImltcG9ydCBrb3RsaW54LmNvcm91dGluZXMuKlxuXG5zdXNwZW5kIGZ1biBtYWluKCkge1xuICAgIHByaW50bG4oXCJIZWxsbyDlpJbpg6ggXCIpXG4gICAgY29yb3V0aW5lU2NvcGUge1xuICAgIHByaW50bG4oXCIxMTExMee6v+eoiyDmmK9cIiArIFRocmVhZC5jdXJyZW50VGhyZWFkKCkpXG4gICAgbGF1bmNoIHsgXG4gICAgICAgcHJpbnRsbihcIjIyMjIy57q/56iLIOaYr1wiICsgVGhyZWFkLmN1cnJlbnRUaHJlYWQoKSlcbiAgICAgICBkZWxheSgxMDAwKVxuICAgICAgIHByaW50bG4oXCJLb3RsaW4gQ29yb3V0aW5lcyBXb3JsZCFcIikgXG4gICAgfVxuICAgIHByaW50bG4oXCIgMzMzMyDnur/nqIsg5pivXCIgKyBUaHJlYWQuY3VycmVudFRocmVhZCgpKVxuICAgIHByaW50bG4oXCJIZWxsb1wiKVxuICAgIH1cbiAgICBwcmludGxuKFwiSGVsbG8g5aSW6YOoIGVuZFwiKVxufSAiLCJwbGF0Zm9ybSI6ImphdmEiLCJhcmdzIjoiIn0=

我們發(fā)現(xiàn)夺饲,在 coroutineScope 中,默認(rèn)是和外部在同一個線程中的施符。而 launch {}會切換到默認(rèn)的一個子線程中 DefaultDispatcher, 而不會影響主線程 println("33333 線程 是"的執(zhí)行往声。

這個代碼中,牽扯到三部分戳吝,

  1. 什么是 coroutineScope()CoroutineScope
  2. 什么是 launch
  3. 什么是 suspend

下面聊一下這三個部分是什么浩销,以及如何使用它們。

1.1 小結(jié)

上述內(nèi)容簡單的介紹了協(xié)程的基本使用以及代碼運行的線程關(guān)系听哭。
同時引入了三個部分:

  • CoroutineScope
  • launch
  • suspend

下面內(nèi)容會依次介紹慢洋。

2. CoroutineScope 類和 coroutineScope(xxx) 方法

CoroutineScope 是一個接口,它為協(xié)程定義了一個范圍「或者稱為 作用域」陆盘,每一個協(xié)程創(chuàng)建者都是它的一個「擴(kuò)展方法」普筹。
上面的說法,意思是什么呢隘马?

  • 1.首先協(xié)程在這個Scope 內(nèi)運行太防,不能超過這個范圍。
  • 2. 協(xié)程只有在 CoroutineScope 才能被創(chuàng)建
    因為目前所有協(xié)程的創(chuàng)建方法酸员, 例如 launch(), async() 全部是 CoroutineScope 的擴(kuò)展方法蜒车。

CoroutineScope 是一個接口, 源碼如下:

/**
* 
*/
public interface CoroutineScope {
    /**
     * The context of this scope.
     * Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope.
     * Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages.
     *
     * By convention, should contain an instance of a [job][Job] to enforce structured concurrency.
     */
    public val coroutineContext: CoroutineContext
}

它里面包含一個成員變量 coroutineContext沸呐, 是當(dāng)前 CoroutineScopecontext.

coroutineContext 可以翻譯成「協(xié)程上下文」醇王,但和 Android 中的 Context 有很大不同呢燥。
CoroutineContext 是一個協(xié)程各種元素的集合崭添。
后面再介紹 CoroutineContext

coroutineScope{}CoroutineScope 不同,coroutineScope{} 是一個方法, 它可以創(chuàng)建一個 CoroutineScope 并在里面運行一些代碼叛氨。

coroutineScope{} 這個會在什么時候結(jié)束呢呼渣?代碼注釋中寫著:

This function returns as soon as the given block and all its children coroutines are completed.

當(dāng)傳入的閉包和它里面所有的子協(xié)程都執(zhí)行完成時才會返回棘伴。因為它是一個 suspend 函數(shù),會在它里面所有的「內(nèi)容」都運行完屁置,才會結(jié)束焊夸。

2.1 CoroutineScope 使用的代碼示例

在源碼的注釋中,寫了它的使用示例蓝角。

2.2.1 在 Activity 中的使用

Activity 里阱穗,你可以這么使用:

class MyActivity : AppCompatActivity(), CoroutineScope by MainScope() {
    override fun onDestroy() {
        cancel() // cancel is extension on CoroutineScope
    }
    
    fun showSomeData() = launch { 
        // <- extension on current activity, launched in the main thread
        // ... here we can use suspending functions or coroutine builders with other dispatchers
       draw(data) // draw in the main thread
    }
}

MyActivity 中實現(xiàn)了 CoroutineScope 接口,并且默認(rèn)是創(chuàng)建了一個 MainScope().

MainScope() 本質(zhì)上是 Creates the main [CoroutineScope] for UI components. 是為主線程上創(chuàng)建了一個 CoroutineScope使鹅,即這個 scope 里的協(xié)程運行在「主線程」(如果未特別指定其他線程的話)

MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

Dispatchers 為「協(xié)程調(diào)度器」揪阶, 后面在介紹它。

上面為源碼中的示例患朱。


2.2.2 在 ViewModel 中使用以及為什么要在 ViewModel 中使用

一般情況下鲁僚,在 Android 我們更愿意把協(xié)程部分放入到 ViewModel 中使用,而不是在 Activity 或者 Fragment 中使用裁厅。

為什么呢? 在上面的示例代碼中冰沙,我們需要在 onDestroy() 中去手動調(diào)用一下 cancel() -> MainScpe 會銷毀里面的協(xié)程。.
而在 ViewModel 中执虹,默認(rèn)有一個擴(kuò)展成員是 ViewModel.viewModelScope, 且它會在 ViewModel 被銷毀時自動回收拓挥, 而 ViewModel 又是和 Activity 生命周期相關(guān)的,因此可以放心大膽使用袋励,會自動銷毀回收撞叽。

同時也是為了把耗時的操作和 UI 剝離,讓代碼更加的清晰, 代碼示例:

class FirstHomeViewModel : ViewModel() {
    ....
    /**
     * 獲取首頁 banner 信息
     */
    fun getBannerData() {
        viewModelScope.launch(IO) {
            // 做一些網(wǎng)絡(luò)請求類似的操作
            ...
            withContext(Main) {
                ...
            }
        }
    }
}

在上述代碼中插龄,我們利用 viewModelScope.launch(IO)IO 線程中創(chuàng)建了一個協(xié)程, 在該協(xié)程里面做一些耗時的操作愿棋,然后通過 withContext(Main) 切換到主線程,可以做一些刷新數(shù)據(jù)和 UI 的操作均牢。

可參考谷歌開源庫 plaid: https://github.com/android/plaid
以及我的另外一篇文章:http://www.reibang.com/p/f5e16605d80c

2.2 ViewModel 自動銷毀 CoroutineScope 的邏輯

todo ViewModel 的自動銷毀

上面我們提到過糠雨,在 ViewModel 中是會自動釋放協(xié)程的,那么是如何實現(xiàn)的呢徘跪?

viewModelScope() 源碼如下:

val ViewModel.viewModelScope: CoroutineScope
        get() {
            val scope: CoroutineScope? = this.getTag(JOB_KEY)
            if (scope != null) {
                return scope
            }
            return setTagIfAbsent(JOB_KEY,
                CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))
        }

其中 setTagIfAbsent(xxx) 會把當(dāng)前 CloseableCoroutineScope 存放在 mBagOfTags 這個 hashMap 中甘邀。

當(dāng) ViewModel 被銷毀時會走 clear() 方法:

MainThread
final void clear() {
    mCleared = true;
    // Since clear() is final, this method is still called on mock objects
    // and in those cases, mBagOfTags is null. It'll always be empty though
    // because setTagIfAbsent and getTag are not final so we can skip
    // clearing it
    if (mBagOfTags != null) {
        synchronized (mBagOfTags) {
            for (Object value : mBagOfTags.values()) {
                // see comment for the similar call in setTagIfAbsent
                closeWithRuntimeException(value);
            }
        }
    }
    onCleared();
}

這里,會把 mBagOfTags 這個 Map 中的所有 value 取出來垮庐,做一個 close 操作松邪,也就是在這里,對我們的 coroutinesScope 做了 close() 操作哨查,從而取消它以及取消它里面的所有協(xié)程逗抑。

2.3 withContext(xxx) 用作切換線程

當(dāng)然,我們使用協(xié)程,很多時候邮府,是需要一些耗時的操作在協(xié)程里面完成荧关,等到這個操作完成后,我們就需要再次切換到主線程執(zhí)行應(yīng)有的邏輯褂傀,那么在協(xié)程里面忍啤,給我們提供了 withContext(xxx) 方法,使我們可以很方便的來回切換到指定的線程仙辟。

有關(guān) withContext(xxx) 的定義:

/**
* Calls the specified suspending block with a given coroutine context, suspends until it completes, and returns the result.
*/
public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T = suspendCoroutineUninterceptedOrReturn sc@ {
    ...
}

方法的含義為:在指定的 coroutineContext 中運行掛起的閉包同波,該方法會一只掛起直到它完成,并且返回閉包的執(zhí)行結(jié)果叠国。
它有兩個參數(shù)参萄,第一個用作指定在那個線程,第二個是要執(zhí)行的閉包邏輯煎饼。

源碼的注釋中還有一句話:This function uses dispatcher from the new context, shifting execution of the [block] into the different thread if a new dispatcher is specified, and back to the original dispatcher when it completes.

翻譯過來就是讹挎,在這個方法中,它會切換到新的調(diào)度器 「在這里可理解為在新的被指定的線程中」里執(zhí)行 block 的代碼吆玖,并且在它完成時筒溃,會自動回到原本的 dispatcher 中。

用更通俗的話就是: withContext() 在執(zhí)行時沾乘,首先會從 A 線程 切換到被你指定的 B 線程中怜奖,然后等到 withContext() 執(zhí)行結(jié)束會,它會自動再切換到 A 線程翅阵。

A->B: 切換線程到 B
B-->A: 執(zhí)行結(jié)束后婆殿,自定切回線程到 A

這也是 withContext() 的方便之處止状, 在 java 代碼中呻澜,沒有這種效果的類似實現(xiàn)据忘。
也因為 withContext() 可以自動把線程切回來的特性,從而消除了一些代碼的嵌套邏輯讹语,使得代碼更易懂钙皮, 再加上 suspend 掛起函數(shù)的特性,代碼瀏覽起來更加舒服顽决。

例如代碼:

fun getBannerData() {
    viewModelScope.launch(IO) {
        Log.i("zc_test", "11111 current thread is ${Thread.currentThread()}")
        withContext(Main) {
            Log.i("zc_test", "22222 current thread is ${Thread.currentThread()}")
        }
        Log.i("zc_test", "33333 current thread is ${Thread.currentThread()}")
    }
}

運行結(jié)果為:

2019-12-19 15:40:51.786 14920-15029/com.chendroid.learning I/zc_test: 11111 current thread is Thread[DefaultDispatcher-worker-3,5,main]

2019-12-19 15:40:51.786 14920-14920/com.chendroid.learning I/zc_test: 22222 current thread is Thread[main,5,main]

2019-12-19 15:40:51.789 14920-15029/com.chendroid.learning I/zc_test: 33333 current thread is Thread[DefaultDispatcher-worker-3,5,main]

「11111」 和 「33333」 兩處位置所在的線程是一致的短条。

2.4 小結(jié)

上面我們寫了很多內(nèi)容,簡單的總結(jié)一下才菠,以防遺忘茸时。

  1. CoroutineScope 是協(xié)程 Coroutine 的作用域,只有在 CoroutineScope 內(nèi)赋访,協(xié)程才可以被創(chuàng)建可都,且協(xié)程只能運行在這個范圍內(nèi)缓待。

  2. ViewModel 具有自動釋放 CoroutineScope 的作用,是生命安全的汹粤。

  3. withContext(xxx) 可在協(xié)程內(nèi)切換線程命斧, 并且具有自動切回原線程的能力田晚。

3. 什么是 launch -- 創(chuàng)建協(xié)程

上面很多地方嘱兼,都或多或少的使用到了 launch() 方法, 那么它到底是什么呢贤徒?有那些需要注意的地方呢芹壕?我們一起來看一下。

launch() 會在當(dāng)前的 coroutineScope 中新建一個協(xié)程接奈,它是開啟一個協(xié)程的一種方式踢涌。

正如在 「什么是 CoroutineScope」 里面說的,launch()CoroutineScope 的一個擴(kuò)展方法序宦。

官方源碼為:

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
}

它接收三個參數(shù): context , start, block睁壁, 返回結(jié)果為 Job

3.1 launch() 的參數(shù)和返回結(jié)果說明
  • contextCoroutineContext:
    用于標(biāo)明當(dāng)前協(xié)程運行的 CoroutineContext,簡單來說就是當(dāng)前 coroutine 運行在哪個調(diào)度器上, 在這里如果不指定的話互捌,默認(rèn)會繼承當(dāng)前 viewModelScope 所在的主線程的主線程調(diào)度器潘明,即「Main = MainCoroutineDispatcher

  • start: CoroutineStart 意思是 coroutine 什么時候開始運行.
    默認(rèn)為 CoroutineStart.DEFAULT, 意思是:立即根據(jù)它的 CoroutineContext 執(zhí)行該協(xié)程。

  • block 閉包秕噪, 會在一個 suspend 掛起函數(shù)里面運行該閉包钳降。
    在閉包中,是我們真正需要執(zhí)行的邏輯腌巾。

  • 返回結(jié)果為 Job :
    用于管理這個協(xié)程遂填,可采用 job.cancel() 來取消這個協(xié)程的運行。

那么什么是 job 呢澈蝙?下面簡單聊一下 Job

3.2 什么是 Job

Job 中文意思是「工作」吓坚, 官方的定義為:它是一個可取消的,其生命周期最終為完成狀態(tài)的事物灯荧。

可以簡單的暫時把它理解為 coroutine 協(xié)程的一個代表凌唬,它可以獲取當(dāng)前協(xié)程的狀態(tài),也可以取消該協(xié)程的運行漏麦。

public interface Job : CoroutineContext.Element {
    ...
}

其實它也是 CoroutineContext 的一個子類客税,「ElementCoroutineContext 的一個子類」。

Job 有三種狀態(tài):

  1. isActive : true 表示該 Job 已經(jīng)開始撕贞,且尚未結(jié)束和被取消掉更耻。
  2. isCompletedtrue 表示該 Job 已經(jīng)結(jié)束「包括失敗和被取消」
  3. isCancelled: true 表示該 Job 被取消掉

在源碼中,有這么一些描述捏膨,可以看作一張圖秧均,我以一個表格的形式展示:
job 有一些狀態(tài)

State isActive isCompleted isCancelled
New (optional initial state) false false false
Active (default initial state) true false false
Completing (transient state) true false false
Cancelling (transient state) false false true
Cancelled (final state) false true true
Completed (final state) false true false

生命周期流程圖:

`job` 生命周期流程圖

從某個角度淺顯的理解食侮,Job 可代指一個協(xié)程 Coroutine 的各種狀態(tài)。


3.3 CoroutineScope.async() 方法

除了 launch() 之外目胡,在協(xié)程中還有一個和它類似的方法用于創(chuàng)建協(xié)程锯七,是 CoroutineScope.async().

async()launch() 的最大不同是返回結(jié)果的不同,launch()是返回一個 job, 而 async() 返回的是 Deferred<T>

Deferred 的翻譯為:「推遲」誉己, 那它是什么呢眉尸?源碼如下:

public interface Deferred<out T> : Job {
    ....
}

額,其實它本身是一個 Job 的子類巨双,也就是說噪猾,DeferredJob 的生命周期流程是一樣的,且也可控制 Coroutine.
它是一個帶著結(jié)果 「resultJob.
可通過調(diào)用 Deferred.await() 等待異步結(jié)果的返回筑累。

我們可以通過 async 實現(xiàn)兩個并發(fā)的網(wǎng)絡(luò)請求袱蜡,例如:

// todo
suspend fun testAsync() {
    coroutineScope {
        val time = measureTimeMillis {
            val one = async { doSomethingsOne() }
            val two = async { doSomethingsTwo() }
            println("the result is ${one.await() + two.await()}")
        }

        println("完成時間為 time is $time ms")
    }
}

private suspend fun doSomethingsOne(): Int {
    // 假設(shè)做了些事情,耗時
    delay(1000L)
    return 17
}

private suspend fun doSomethingsTwo(): Int {
    // 假設(shè)做了些事情慢宗,耗時
    delay(1000L)
    return 30
}

運行結(jié)果為下:

the result is 47
完成時間為 time is 1017 ms

這里時間是小于 2000 ms 的坪蚁,原因就是上面兩個協(xié)程是并發(fā)運行的。

當(dāng)然 await() 也是一個掛起函數(shù)

3.4 小結(jié)

上面內(nèi)容中镜沽,我們總結(jié)了

  1. launch() 的作用—— 是用來新建一個協(xié)程敏晤。
  2. launch() 中各個參數(shù)的函數(shù);
  3. launch() 的返回結(jié)果 job 的意義淘邻,以及它能夠獲取到當(dāng)前協(xié)程的各種狀態(tài)
  4. 創(chuàng)建協(xié)程的另外一種方式:async() 的簡單說明

4. 什么是 suspend

我們已經(jīng)無數(shù)次在前面提到 suspend 掛起函數(shù)了茵典,那么「掛起函數(shù)」到底是代表著什么意思呢?「非阻塞掛起」又是什么意思呢宾舅?

4.1 「掛起函數(shù)」的使用和代碼運行分析

suspendkotlin 中的一個關(guān)鍵字统阿,它本身的意思是「掛起」。
kotlin 中筹我,被它標(biāo)注的函數(shù)扶平,被稱為「掛起函數(shù)」。

suspend function should be called only from a coroutine or another suspend function

首先「掛起函數(shù)」只能在協(xié)程和另外一個掛起函數(shù)里面調(diào)用蔬蕊。

4.1.1 同一線程中代碼運行邏輯

以下面代碼為例结澄,假設(shè)三個方法都在同一個線程「主線程」運行:

a()
b()
c()

正常的同一線程的代碼邏輯,原本就是阻塞式的岸夯,:

  1. a() 運行結(jié)束后麻献,b() 開始運行;
  2. b() 運行結(jié)束后猜扮,c() 開始運行勉吻;
a()->b(): a() 運行結(jié)束后 b() 執(zhí)行
b()->c(): b() 運行結(jié)束后 c() 執(zhí)行

4.1.2 在當(dāng)前線程中新建一個線程的代碼運行邏輯--未使用 suspend

如果 b() 中開啟了一個子線程去處理邏輯「異步了」,且不使用 suspend 標(biāo)注 b() 的代碼塊運行邏輯為:

  1. a() 運行結(jié)束后旅赢,b() 開始運行齿桃;
  2. b() 函數(shù)中惑惶,部分在主線程中的代碼運行完后,它開啟的子線程代碼可能還沒運行短纵,c() 開始執(zhí)行
a()->b(): a() 運行結(jié)束后 b() 執(zhí)行
b()->c(): b() 中带污,在主線程運行結(jié)束后「子線程可能剛開始還沒結(jié)束」, c() 執(zhí)行

上述代碼香到,其實是說鱼冀,b() 的異步代碼可能會晚與 c() 去執(zhí)行,因為異步和兩個線程养渴,導(dǎo)致代碼不再阻塞雷绢。

4.1.3 使用了 suspend 標(biāo)注泛烙, 代碼的運行邏輯

  1. a() 運行結(jié)束后理卑,b() 開始運行;
  2. b() 運行結(jié)束后「它的子線程也運行結(jié)束了」蔽氨,c() 才會開始運行藐唠;
a()->b(): a() 運行結(jié)束后 b() 執(zhí)行
b()->c(): b() 運行結(jié)束后 c() 執(zhí)行

可以看到使用了 suspend 標(biāo)注的函數(shù),會使得當(dāng)前代碼在該函數(shù)處處于等待它的完全運行結(jié)束鹉究。

suspend 掛起函數(shù)的完全運行結(jié)束是指:該函數(shù)中的所有代碼「可能包含一個新的子線程宇立、」均運行結(jié)束。

上述三中不同的代碼的運行自赔,其實是想告訴大家 suspend 這個關(guān)鍵字的作用是:
把原本異步的代碼妈嘹,再次變得同步

當(dāng)天如果只是簡單的同步绍妨,那么肯定會有很多問題润脸,
例如主線程等待子線程運行結(jié)束的問題,這是很不科學(xué)的他去,與我們把耗時操作放入子線程運行的初衷不符毙驯。

當(dāng)然,協(xié)程當(dāng)然不存在這種問題灾测。它是如何解決的呢爆价?

下面說一說協(xié)程的 「非阻塞掛起」

4.2 「非阻塞掛起」

我們還以第三種代碼情況說明, 不過這次加入了更多的代碼 test2() 方法。

假設(shè)完整代碼為:
b()suspend 標(biāo)注的掛起函數(shù), 其他為正常函數(shù)
以下為簡化代碼

fun mainTest() {
    ...
    test()
    test2() // 假設(shè) test2() 運行在主線程
    ...
}

fun test() {
    a()
    b()
    c()
}

fun test2() {
    ...
}

代碼實際的執(zhí)行運行邏輯為:

  1. mainTest() 中先執(zhí)行到 test() 方法媳搪,先運行 a()
  2. a() 運行結(jié)束后铭段,「掛起函數(shù)」b() 開始運行;
  3. 「掛起函數(shù)」b() 的主線程代碼運行結(jié)束后秦爆,c() 并不會運行序愚,而是 test2()開始運行,
  4. 等到「掛起函數(shù)」 b() 中開啟的子線程也運行結(jié)束后鲜结,c() 才會開始運行展运;

圖示為:

mainTest()->test(): 先執(zhí)行 test() 「主線程」
test()->a(): 順序執(zhí)行 a() 「主線程」
a()->b(): a() 結(jié)束后活逆,執(zhí)行掛起函數(shù) b() 「主線程」
b()-->test(): b() 中的主線程完成后,在切到子線程時拗胜,會標(biāo)志 test() 執(zhí)行結(jié)束 「主線程」
test()-->mainTest(): test() 執(zhí)行結(jié)束蔗候,會順序執(zhí)行 test2(), 「主線程」
b()->c(): 注釋 1

注:上圖中的注釋 1 為:當(dāng)掛起函數(shù) b() 里面的子線程運行結(jié)束后,會被協(xié)程切換到主線程埂软,然后 c() 開始運行锈遥。

從上面可以看到 suspend 的作用是在當(dāng)前代碼處 「1」暫停運行,轉(zhuǎn)而去運行該線程本身其他地方的邏輯代碼勘畔,等到該掛起函數(shù)中的代碼運行結(jié)束后「它里面的和它里面的子線程子協(xié)程均運行結(jié)束后」所灸,才會在暫停處 「1」 繼續(xù)運行。

注: 上述代碼炫七,其實并不完全成立爬立,因為只能在「協(xié)程」或者「掛起函數(shù)」里面才可以調(diào)用「掛起函數(shù)」 b() , 因此 test() 并不成立万哪,這里用于說明代碼運行邏輯侠驯,故而簡化了代碼。后面會給出完整的代碼奕巍。

哪里可以提現(xiàn)出:「非阻塞式掛起」這個含義呢吟策?

就是因為在上面的代碼中,在 test() 中的 b() 處掛起時「本身為主線程」的止,并不會影響到主線程的執(zhí)行檩坚,因為 test2() 在主線程中為正常執(zhí)行,阻塞的只是該協(xié)程內(nèi)部的代碼诅福。

4.3 附上完全測試代碼以及執(zhí)行結(jié)果

代碼為:

fun test {
    viewModelScope.launch {
        println("viewModelScope.launch ${Thread.currentThread()}")
        mainTest()
        println("viewModelScope.launch 結(jié)束了 ${Thread.currentThread()}")
    }
    
    test2()
}
...
// mainTest() 方法
suspend fun mainTest() {
    println("mainTest() start start start " + Thread.currentThread())
    a()
    b()
    c()
    println("mainTest() end end end" + Thread.currentThread())
}

// 普通函數(shù) test2()
fun test2() {
    println("test2() doing doing doing " + Thread.currentThread())
}
//普通函數(shù) a() 
fun a() {
    println("a() doing doing doing " + Thread.currentThread())
}
//普通函數(shù) c() 
fun c() {
    println("c() doing doing doing " + Thread.currentThread())
}
// 掛起函數(shù) b()
suspend fun b() {
    println("b() start start start" + Thread.currentThread())
    coroutineScope {
        println("11111 線程 是" + Thread.currentThread())
        launch(IO) {
           println("22222 線程 是" + Thread.currentThread())
           delay(1000)
           println("22222 線程結(jié)束" + Thread.currentThread())
        }
        println("33333 線程 是" + Thread.currentThread())
    }
    println("b() end end end" + Thread.currentThread())
}

運行結(jié)果為:

I/System.out: viewModelScope.launch Thread[main,5,main]
I/System.out: mainTest() start start start Thread[main,5,main]
    a() doing doing doing Thread[main,5,main]
    b() start start startThread[main,5,main]
I/System.out: 11111 線程 是Thread[main,5,main]
I/System.out: 33333 線程 是Thread[main,5,main]
I/System.out: 22222 線程 是Thread[DefaultDispatcher-worker-2,5,main] 「標(biāo)注 1」
I/System.out: test2() doing doing doing Thread[main,5,main]         「標(biāo)注 2」
I/System.out: 22222 線程結(jié)束Thread[DefaultDispatcher-worker-9,5,main]
I/System.out: b() end end endThread[main,5,main]
    c() doing doing doing Thread[main,5,main]
    mainTest() end end endThread[main,5,main]
    viewModelScope.launch 結(jié)束了 Thread[main,5,main]

可以看到 test2() 的執(zhí)行是要早于 c() 方法的匾委。

從運行結(jié)果上可以看到是和我們的分析一致的。

4.4 suspend b() 運行時的線程切換

從運行結(jié)果的 log 上, 我們還可以看到當(dāng)前代碼執(zhí)行的線程信息权谁。

我們發(fā)現(xiàn) suspend b() 的運行中剩檀,

  1. b() start ... 在主線程 main
  2. 通過 b() 中的 launch(IO) 我們切換到了 IO 線程 DefaultDispatcher-worker
  3. 但是 b() 中的子線程運行結(jié)束后,我們發(fā)現(xiàn) b() end 再次回答了主線程 main

在上面的操作中旺芽,第三步中沪猴,我們并沒有顯示的調(diào)用切回主現(xiàn)場的代碼,我們卻回到了主線程采章。

由此說明:suspend 掛起函數(shù)在運行結(jié)束時會再次切換到原來的線程运嗜,真正的切換是有協(xié)程幫我們做的

值得一提的是,我們在上面說到 withContext() 也具有自動切換原線程的功能悯舟。
因為……
withContext() 本身就是一個「掛起函數(shù)」担租。
協(xié)程是怎么切換到原線程的呢?一家之言抵怎,我害怕說不清楚……慫

4.5 這里插入一個小的點奋救。

根據(jù)上面岭参,我們知道 suspend 標(biāo)注的掛起函數(shù),協(xié)程會自動幫我們切換到原線程尝艘。
看兩行 log 信息

...
I/System.out: 22222 線程 是Thread[DefaultDispatcher-worker-2,5,main]
...
I/System.out: 22222 線程結(jié)束Thread[DefaultDispatcher-worker-9,5,main]
  1. 首先 Thread[DefaultDispatcher-worker-2,5,main] 這三項分別是什么

    大部分人應(yīng)該都知道演侯,這是源碼 Thread.toString() 方法中的返回值.
    第一個參數(shù) DefaultDispatcher-worker-2 代表的是當(dāng)前線程的名字 getName().
    第二個參數(shù) 5 代表的是當(dāng)前線程的優(yōu)先級 getPriority() 默認(rèn)是 5.
    第三個參數(shù) main 代表的是當(dāng)前線程屬于哪個線程組。

  1. 為什么先后兩次線程會不一致背亥?

    在下面的 5 部分 CoroutineDispatcher 我們會有介紹秒际,IO 調(diào)度器,它里面對應(yīng)的是一個線程池狡汉。所以先后兩次線程名字不一樣娄徊。
    但它們屬于同一線程池
    **也屬于同一個調(diào)度器 DefaultDispatcher **

帶來了一個問題,為什么在一個協(xié)程中盾戴,先后兩次線程的名字不同了呢寄锐?

肯定是在哪里切換了線程,才會導(dǎo)致線程的名稱不同捻脖。
看代碼中锐峭,我們知道:

  1. 22222 線程 是22222 線程結(jié)束 是在同一個 launch(IO){} 協(xié)程內(nèi)的;

  2. 由于 delay() 是個 suspend 掛起函數(shù)中鼠,根據(jù)上面的 4.4 中的描述可婶,協(xié)程在「掛起函數(shù)」運行完成后,自動幫我們切回原線程援雇,但打印的結(jié)果表示其實在了另外一個線程中矛渴。

    所以更準(zhǔn)確得說法是:

  3. 協(xié)程在「掛起函數(shù)」運行結(jié)束后,會自動切回原來的調(diào)度器中惫搏。
    然后調(diào)度器可能會根據(jù)它對應(yīng)的線程池具温,去選擇可用的線程繼續(xù)工作。

這里需要涉及到 CoroutineDispatcher 以及 ContinuationInterceptor筐赔,這里不做過多介紹「內(nèi)容實在太多了……懶~」铣猩。

記住一點就行:所有協(xié)程啟動時「掛起后,再次運行也為啟動」茴丰,都會有一次 Continuation.resumeWith() 的操作达皿,這時調(diào)度器會重新調(diào)度一次,協(xié)程的運行可能會從線程池中的 A 線程切換到 B 這個線程上贿肩。

這也是上述 log 信息出現(xiàn)的線程名字不同的原因峦椰。

Continuation 的源碼如下:

/**
 * Interface representing a continuation after a suspension point that returns a value of type `T`.
 */
SinceKotlin("1.3")
public interface Continuation<in T> {
    /**
     * The context of the coroutine that corresponds to this continuation.
     */
    public val context: CoroutineContext

    /**
     * Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the
     * return value of the last suspension point.
     */
    public fun resumeWith(result: Result<T>)
}

在有一個 suspend 掛起點后,它會代表著一個協(xié)程汰规,協(xié)程會存在 T 中汤功,通過 resumeWith(result: Result<T>) 會重新得到這個協(xié)程實例。

4.6 suspend 小結(jié)

上面溜哮,我們使用了大量的代碼和邏輯圖滔金,用于表示 suspend 在實際運行中起到的作用色解。

  • suspend 會使得當(dāng)前代碼的運行在該函數(shù)處「掛起「協(xié)程內(nèi)掛起」」。

  • suspend 的掛起餐茵,并不會影響主線程的代碼執(zhí)行冒签,掛起的范圍也是我們上面提到的 CoroutineScope 這個范圍內(nèi)。

  • suspend 掛起函數(shù)具有在該函數(shù)運行結(jié)束后钟病,再次切回原線程的能力萧恕。當(dāng)然,這是協(xié)程內(nèi)部幫我們完成的肠阱。

  • 更準(zhǔn)確的說法是:協(xié)程會在掛起函數(shù)運行結(jié)束后票唆,自動切回原調(diào)度器的能力。

那么「調(diào)度器」 是指什么呢屹徘?下面簡單說一下走趋。


5. CoroutineDispatcher 協(xié)程中的調(diào)度器

首先它繼承于 AbstractCoroutineContextElement, 并實現(xiàn)了 ContinuationInterceptor 接口。

它是 CoroutineContext 的一個子類噪伊。

上面的代碼分析中簿煌,我們使用的 launch(), async(), 有時我們傳遞了一個參數(shù)「Main, IO」,其實就是 CoroutineDispatcher 鉴吹。

在上面中姨伟,我們已經(jīng)見到了 Main IO 兩個調(diào)度器。

ContinuationInterceptor 是協(xié)程攔截器豆励, 在這里暫時不討論它夺荒。

5.1 CoroutineDispatcher 的種類

CoroutineDispatcher 的種類,都在 Dispatchers 類里面良蒸,在 Android 中有一下四類:

  1. Default: CoroutineDispatcher = createDefaultDispatcher()

    默認(rèn)的調(diào)度器技扼, 在 Android 中對應(yīng)的為「線程池」。
    在新建的協(xié)程中嫩痰,如果沒有指定 dispatcherContinuationInterceptor 則默認(rèn)會使用該 dispatcher剿吻。
    線程池中會有多個線程。

  2. Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher

    在主線程「UI 線程」中的調(diào)度器串纺。
    只在主線程中, 單個線程丽旅。

  3. Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined

  1. IO: CoroutineDispatcher = DefaultScheduler.IO

    IO 線程的調(diào)度器,里面的執(zhí)行邏輯會運行在 IO 線程, 一般用于耗時的操作造垛。
    對應(yīng)的是「線程池」魔招,會有多個線程在里面。IODefault 共享了線程五辽。


6. 說一說協(xié)程里面常見的類

在文章的開頭办斑,有一張圖,里面有一些在協(xié)程中涉及到的類,現(xiàn)在再來看一下乡翅。

常見類繼承圖

是不是比剛在文章的開頭看上去要親和很多鳞疲?

如果是,那么恭喜你蠕蚜,說明大部分內(nèi)容你都看到了尚洽,并且記在了心里,這么長且枯燥的內(nèi)容靶累,很看到這里都很不容易腺毫。贊的贊的

6.1 CoroutineContext

CoroutineContext 和我們經(jīng)常在代碼中使用到的 Context 差別是很大的,它們兩沒有任何關(guān)系挣柬。

CoroutineContext 是各種不同元素的集合潮酒。

源碼如下:

/**
 * Persistent context for the coroutine. It is an indexed set of [Element] instances.
 * An indexed set is a mix between a set and a map.
 * Every element in this set has a unique [Key].
 */
SinceKotlin("1.3")
public interface CoroutineContext {
    ...
    public operator fun <E : Element> get(key: Key<E>): E?
    ...
    /**
     * Key for the elements of [CoroutineContext]. [E] is a type of element with this key.
     */
    public interface Key<E : Element>
    /**
     * An element of the [CoroutineContext]. An element of the coroutine context is a singleton context by itself.
     */
    public interface Element : CoroutineContext {
        ...
    }
}

它的繼承關(guān)系是怎樣的呢?

CoroutineContext 的繼承關(guān)系

什么是 Element? 什么是 Key?

  1. Element 是一個接口邪蛔,實現(xiàn)了 CoroutineContext,
    代表著:CoroutineContext 的一個元素急黎,且為一個單例侧到。

  2. Key 是以 Element 作為 key 的接口。

CoroutineContext 需要根據(jù) Key 獲取到它對應(yīng)的 Element

例如:

// 獲取當(dāng)前協(xié)程的 job
val job = coroutineContext[Job]
val continuationInterceptor = coroutineContext[ContinuationInterceptor]

如果你翻一翻源碼就會發(fā)現(xiàn)故源,在 JobContinuationInterceptor 中,必定會實現(xiàn) CoroutineContext.Element 接口心软,并且具有一個「伴生對象」 companion object Key : CoroutineContext.Key<XXX>著蛙。

JobCoroutineContext 中最為重要的元素耳贬,代表著協(xié)程的運行狀態(tài)等信息

6.2 CoroutineContinuation

Coroutine 就是我們說的「協(xié)程」踏堡, CoroutineScope.launch() 是會創(chuàng)建一個 Coroutine 的實例。

Continuation 是延續(xù)的意思咒劲,當(dāng)一個協(xié)程被創(chuàng)建時顷蟆,就會有一個 Continuation 對應(yīng)著該協(xié)程,它也可代表著協(xié)程的狀態(tài)腐魂。

用下面的圖表示協(xié)程的繼承關(guān)系:

coroutine 的繼承圖

我們可以發(fā)現(xiàn) Coroutine 繼承和實現(xiàn)了大量的接口帐偎,有 Job,Continuation, CoroutineScope

目前創(chuàng)建的協(xié)程,如果不特別指定蛔屹,都是 StandaloneCoroutine 的實例削樊,會立馬執(zhí)行。

當(dāng)掛起后,需要重新執(zhí)行協(xié)程時漫贞,會調(diào)用 Continuation.resume() 再次得到該協(xié)程實例甸箱,然后開始調(diào)度運行。

7. 總結(jié)

一定要先說一句迅脐,一家之言芍殖,很多理解可能并不準(zhǔn)確,有錯誤還請指正谴蔑。
協(xié)程庫里面的元素太多了豌骏,上面我只是從使用的 API 接口入口,逐步介紹了涉及到的一些知識隐锭。

但協(xié)程里面的實現(xiàn)原理肯适,調(diào)度器,切換原調(diào)度器的操作等原理成榜,都未進(jìn)行深入說明。

協(xié)程內(nèi)容太多了刘绣,想到這里纬凤,已經(jīng)比我剛開始想的要多很多很多停士。

目前寫到的內(nèi)容恋技,也只是淺嘗輒止蜻底。

但我真心希望薄辅,這篇花費了大量時間去寫的文章站楚,能解決一些對協(xié)程的困惑,能對看到這篇文章的人起到幫助。

希望能盡快用起來協(xié)程舅踪,真正使用起來抽碌,就能明顯感受到它給代碼帶來的精簡和便利货徙。

參考文檔:

朱凱-協(xié)程
medium - easy coroutines

http://talentprince.github.io/2019/02/12/Deep-explore-kotlin-coroutines/

Kotlin1.3 協(xié)程Api詳解:CoroutineScope, CoroutineContext

破解 Kotlin 協(xié)程(3) - 協(xié)程調(diào)度篇

Kotlin 協(xié)程之二:原理剖析

2019.12.26 by chendroid

這本是之前寫的文章了蠢棱,無奈元旦之前未能發(fā)出泻仙,趕在 2020 的開始玉转,發(fā)出來究抓。

祝 2020 年刺下,每個人都能付出得到收獲怠李。

所有的愿望都將實現(xiàn),如果你有勇氣追求它

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市惕鼓,隨后出現(xiàn)的幾起案子矾飞,更是在濱河造成了極大的恐慌洒沦,老刑警劉巖申眼,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件巷蚪,死亡現(xiàn)場離奇詭異屁柏,居然都是意外死亡淌喻,警方通過查閱死者的電腦和手機(jī)似嗤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來伤塌,“玉大人每聪,你說我怎么就攤上這事药薯⊥荆” “怎么了穷娱?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵泵额,是天一觀的道長篓叶。 經(jīng)常有香客問我澜共,道長嗦董,這世上最難降的妖魔是什么京革? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任匹摇,我火速辦了婚禮廊勃,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘画侣。我一直安慰自己配乱,他們只是感情好桑寨,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布西疤。 她就那樣靜靜地躺著代赁,像睡著了一般芭碍。 火紅的嫁衣襯著肌膚如雪窖壕。 梳的紋絲不亂的頭發(fā)上瞻讽,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機(jī)與錄音烦磁,去河邊找鬼都伪。 笑死陨晶,一個胖子當(dāng)著我的面吹牛先誉,可吹牛的內(nèi)容都是我干的谆膳。 我是一名探鬼主播漱病,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼注盈!你這毒婦竟也來了老客?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤鳍鸵,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后哲嘲,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體画切,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡槽唾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了忘闻。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片私恬。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡本鸣,死狀恐怖荣德,靈堂內(nèi)的尸體忽然破棺而出涮瞻,到底是詐尸還是另有隱情署咽,我是刑警寧澤宁否,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布家淤,位于F島的核電站絮重,受9級特大地震影響青伤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蚪腋,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一立帖、第九天 我趴在偏房一處隱蔽的房頂上張望晓勇。 院中可真熱鬧绑咱,春花似錦描融、人聲如沸窿克。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽改执。三九已至衬横,卻和暖如春蜂林,著一層夾襖步出監(jiān)牢的瞬間噪叙,已是汗流浹背睁蕾。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工子眶, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留臭杰,地道東北人硅卢。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓将塑,卻偏偏與公主長得像点寥,于是被迫代替她去往敵國和親敢辩。 傳聞我的和親對象是個殘疾皇子戚长,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容