協(xié)程 Kotlin Coroutine 初探

協(xié)程 kotlin Coroutine

目錄:

1. Coroutine 的基本使用

1.1 小結

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 小結
3. launch -> 創(chuàng)建協(xié)程
  • 3.1 launch() 的參數(shù)和返回結果說明
  • 3.2 什么是 Job
  • 3.3 CoroutineScope.async() 方法
  • 3.4 小結
4. suspend 是什么,「掛起」作用是什么
  • 4.1 「掛起函數(shù)」的使用和代碼運行分析
    • 4.1.1 同一線程中代碼運行邏輯
    • 4.1.2 在當前線程中新建一個線程的代碼運行邏輯--未使用 suspend
    • 4.1.3 使用了 suspend 標注, 代碼的運行邏輯
  • 4.2 「非阻塞掛起」的含義
  • 4.3 完整測試代碼以及執(zhí)行結果
  • 4.4 suspend b() 運行時的線程切換
  • 4.5 插入一個小點:調(diào)度器和線程池
  • 4.6 「掛起函數(shù)」小結
5. 調(diào)度器 CoroutineDispatcher
  • 5.1 CoroutineDispatcher 的種類
6. 說一說協(xié)程中常見的類
  • 6.1 CoroutineContext 的繼承關系
  • 6.2 Coroutine 的繼承關系
7. 總結

正文

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

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

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

api 引入.png

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

常見類繼承圖

1. Coroutine 的基本使用

官方示例代碼如下:

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

代碼運行結果如下:

Hello
Kotlin Coroutines World!

從運行結果來看,launch{} 中的代碼應該和外面的代碼不再同一個線程,下面我們驗證一下。

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

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

這是代碼運行結果為:

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 中,默認是和外部在同一個線程中的。而 launch {}會切換到默認的一個子線程中 DefaultDispatcher, 而不會影響主線程 println("33333 線程 是"的執(zhí)行。

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

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

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

1.1 小結

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

  • CoroutineScope
  • launch
  • suspend

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

2. CoroutineScope 類和 coroutineScope(xxx) 方法

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

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

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, 是當前 CoroutineScopecontext.

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

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

coroutineScope{} 這個會在什么時候結束呢?代碼注釋中寫著:

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

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

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 接口,并且默認是創(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 中,默認有一個擴展成員是 ViewModel.viewModelScope, 且它會在 ViewModel 被銷毀時自動回收, 而 ViewModel 又是和 Activity 生命周期相關的,因此可以放心大膽使用,會自動銷毀回收。

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

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

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

可參考谷歌開源庫 plaid: https://github.com/android/plaid
以及我的另外一篇文章:http://www.itdecent.cn/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) 會把當前 CloseableCoroutineScope 存放在 mBagOfTags 這個 hashMap 中。

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) 用作切換線程

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

有關 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í)行結果。
它有兩個參數(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í)行結束會,它會自動再切換到 A 線程。

A->B: 切換線程到 B
B-->A: 執(zhí)行結束后,自定切回線程到 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()}")
    }
}

運行結果為:

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 小結

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

  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() 會在當前的 coroutineScope 中新建一個協(xié)程,它是開啟一個協(xié)程的一種方式。

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

官方源碼為:

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, 返回結果為 Job

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

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

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

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

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

3.2 什么是 Job

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

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

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

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

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

  1. isActive : true 表示該 Job 已經(jīng)開始,且尚未結束和被取消掉。
  2. isCompletedtrue 表示該 Job 已經(jīng)結束「包括失敗和被取消」
  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() 的最大不同是返回結果的不同,launch()是返回一個 job, 而 async() 返回的是 Deferred<T>

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

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

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

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

// 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 {
    // 假設做了些事情,耗時
    delay(1000L)
    return 17
}

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

運行結果為下:

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

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

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

3.4 小結

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

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

4. 什么是 suspend

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

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

suspendkotlin 中的一個關鍵字,它本身的意思是「掛起」。
kotlin 中,被它標注的函數(shù),被稱為「掛起函數(shù)」。

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

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

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

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

a()
b()
c()

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

  1. a() 運行結束后,b() 開始運行;
  2. b() 運行結束后,c() 開始運行;
a()->b(): a() 運行結束后 b() 執(zhí)行
b()->c(): b() 運行結束后 c() 執(zhí)行

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

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

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

上述代碼,其實是說,b() 的異步代碼可能會晚與 c() 去執(zhí)行,因為異步和兩個線程,導致代碼不再阻塞。

4.1.3 使用了 suspend 標注, 代碼的運行邏輯

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

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

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

上述三中不同的代碼的運行,其實是想告訴大家 suspend 這個關鍵字的作用是:
把原本異步的代碼,再次變得同步。

當天如果只是簡單的同步,那么肯定會有很多問題,
例如主線程等待子線程運行結束的問題,這是很不科學的,與我們把耗時操作放入子線程運行的初衷不符。

當然,協(xié)程當然不存在這種問題。它是如何解決的呢?

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

4.2 「非阻塞掛起」

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

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

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

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

fun test2() {
    ...
}

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

  1. mainTest() 中先執(zhí)行到 test() 方法,先運行 a()
  2. a() 運行結束后,「掛起函數(shù)」b() 開始運行;
  3. 「掛起函數(shù)」b() 的主線程代碼運行結束后,c() 并不會運行,而是 test2()開始運行,
  4. 等到「掛起函數(shù)」 b() 中開啟的子線程也運行結束后,c() 才會開始運行;

圖示為:

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

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

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

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

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

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

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

代碼為:

fun test {
    viewModelScope.launch {
        println("viewModelScope.launch ${Thread.currentThread()}")
        mainTest()
        println("viewModelScope.launch 結束了 ${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 線程結束" + Thread.currentThread())
        }
        println("33333 線程 是" + Thread.currentThread())
    }
    println("b() end end end" + Thread.currentThread())
}

運行結果為:

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] 「標注 1」
I/System.out: test2() doing doing doing Thread[main,5,main]         「標注 2」
I/System.out: 22222 線程結束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 結束了 Thread[main,5,main]

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

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

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

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

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

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

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

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

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

4.5 這里插入一個小的點。

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

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

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

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

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

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

肯定是在哪里切換了線程,才會導致線程的名稱不同。
看代碼中,我們知道:

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

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

    所以更準確得說法是:

  3. 協(xié)程在「掛起函數(shù)」運行結束后,會自動切回原來的調(diào)度器中。
    然后調(diào)度器可能會根據(jù)它對應的線程池,去選擇可用的線程繼續(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 小結

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

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

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

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

  • 更準確的說法是:協(xié)程會在掛起函數(shù)運行結束后,自動切回原調(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()

    默認的調(diào)度器, 在 Android 中對應的為「線程池」。
    在新建的協(xié)程中,如果沒有指定 dispatcherContinuationInterceptor 則默認會使用該 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 線程, 一般用于耗時的操作。
    對應的是「線程池」,會有多個線程在里面。IODefault 共享了線程。


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

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

常見類繼承圖

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

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

6.1 CoroutineContext

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

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 {
        ...
    }
}

它的繼承關系是怎樣的呢?

CoroutineContext 的繼承關系

什么是 Element? 什么是 Key?

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

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

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

例如:

// 獲取當前協(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ù)的意思,當一個協(xié)程被創(chuàng)建時,就會有一個 Continuation 對應著該協(xié)程,它也可代表著協(xié)程的狀態(tài)。

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

coroutine 的繼承圖

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

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

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

7. 總結

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

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

協(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),如果你有勇氣追求它

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容