一學(xué)就會的協(xié)程使用——基礎(chǔ)篇(七)初遇結(jié)構(gòu)化

1. 引言

前文提及了join函數(shù),那么進(jìn)一步的便是協(xié)程中非常強(qiáng)大的結(jié)構(gòu)化并發(fā)設(shè)計(jì)了。

結(jié)構(gòu)化并發(fā)(structured concurrency)從字面上并不直觀能理解,本文將通過實(shí)踐代碼來介紹。

2. 巧妙地等待協(xié)程完成

前面有所介紹,希望等待某一個協(xié)程執(zhí)行完成時,可以使用Job#join(),那么自然而言地,如果需要等個多個協(xié)程,那么就將每次啟動的協(xié)程Job對象收集或保存起來再逐個調(diào)用即可。

嗯,這樣的確可以在功能上達(dá)到目的。但是,想一下,在協(xié)程取消剛開始時我們是從Job維度去實(shí)現(xiàn)取消的,這當(dāng)然可以實(shí)現(xiàn)取消的需要,但是在進(jìn)行到協(xié)程作用域部分的實(shí)踐時,便是不再通過Job的維度去取消,而是通過協(xié)程作用域的維度。那么,可以想想,在等待協(xié)程完成中是否也有這種功能呢?

事實(shí)上,卻是可以有更好的選擇,那便是結(jié)構(gòu)化并發(fā)。

上代碼前,假定一個場景吧:某一時刻要進(jìn)行三個耗時的操作,三個操作之間互不干擾,三個操作都執(zhí)行完成后再進(jìn)行下一步執(zhí)行。

這里假定的耗時操作實(shí)踐代碼如下:

private suspend fun testIOCoroutine(calledMsg: String) {
    "Coroutine IO runs ($calledMsg)".let {
        myLog(it)
    }
    /* 僅為樣例代碼,休眠線程其實(shí)是非常非常不建議的做法??! */
    Thread.sleep(randomMilli)
    "Coroutine IO runs after thread sleep ($calledMsg)".let {
        myLog(it)
    }
}

本質(zhì)上是以將當(dāng)前線程休眠一個隨機(jī)的時間,為了使時間代碼更有趣些,每次調(diào)用時休眠的時間這里其實(shí)是5000毫秒到10000毫秒之間的隨機(jī)值:

val randomMilli: Long
    get() = (FIVE_SECONDS..TEN_SECONDS).random()

接下來按照假定的場景,分別啟動三個協(xié)程進(jìn)行三次耗時操作調(diào)用,并且有相應(yīng)的log輸出代碼:

private fun launchStructuredBtnClicked() {
    "launchStructuredBtnClicked".let {
        myLog(it)
    }
    job?.cancel()
    job = scope.launch {
        "Coroutine Main runs (launchStructuredBtnClicked)".let {
            myLog(it)
        }
        launch(Dispatchers.IO) {
            testIOCoroutine("launchStructuredBtnClicked A")
        }
        launch(Dispatchers.IO) {
            testIOCoroutine("launchStructuredBtnClicked B")
        }
        launch(Dispatchers.IO) {
            testIOCoroutine("launchStructuredBtnClicked C")
        }
        "Coroutine Main runs final statement (launchStructuredBtnClicked)".let {
            myLog(it)
        }
    }
}

這里先保留個問題:明明只需要3個協(xié)程去執(zhí)行代碼,為什么這里啟動了4個協(xié)程?

可以看到,只有最外面scope.launch啟動的協(xié)程Job對象被記錄下來了,里面啟動的3個協(xié)程Job對象并沒有被記錄。

最后,還是等待協(xié)程完成,同樣也是Job#join()的調(diào)用:

private fun joinBtnClicked() {
    "joinBtnClicked".let {
        myLog(it)
    }
    scope.launch(Dispatchers.Main) {
        "Coroutine Main runs (joinBtnClicked)".let {
            myLog(it)
        }
        val jobNonNull = job ?: return@launch
        jobNonNull.join()
        "Coroutine Main runs after join() (joinBtnClicked)".let {
            myLog(it)
        }
    }
}

先點(diǎn)擊launchStructuredBtn再點(diǎn)擊joinBtn的話,某一次執(zhí)行的結(jié)果log輸出如下:

20:35:26.665 D/chenhj: launchStructuredBtnClicked ::running in Thread:[id:2][name:main]
20:35:26.687 D/chenhj: Coroutine Main runs (launchStructuredBtnClicked) ::running in Thread:[id:2][name:main]
20:35:26.688 D/chenhj: Coroutine IO runs (launchStructuredBtnClicked A) ::running in Thread:[id:3037][name:DefaultDispatcher-worker-3]
20:35:26.688 D/chenhj: Coroutine IO runs (launchStructuredBtnClicked B) ::running in Thread:[id:3036][name:DefaultDispatcher-worker-2]
20:35:26.688 D/chenhj: Coroutine Main runs final statement (launchStructuredBtnClicked) ::running in Thread:[id:2][name:main]
20:35:26.688 D/chenhj: Coroutine IO runs (launchStructuredBtnClicked C) ::running in Thread:[id:3039][name:DefaultDispatcher-worker-5]
20:35:27.843 D/chenhj: joinBtnClicked ::running in Thread:[id:2][name:main]
20:35:27.847 D/chenhj: Coroutine Main runs (joinBtnClicked) ::running in Thread:[id:2][name:main]
20:35:32.726 D/chenhj: Coroutine IO runs after thread sleep (launchStructuredBtnClicked C) ::running in Thread:[id:3039][name:DefaultDispatcher-worker-5]
20:35:33.684 D/chenhj: Coroutine IO runs after thread sleep (launchStructuredBtnClicked A) ::running in Thread:[id:3037][name:DefaultDispatcher-worker-3]
20:35:36.475 D/chenhj: Coroutine IO runs after thread sleep (launchStructuredBtnClicked B) ::running in Thread:[id:3036][name:DefaultDispatcher-worker-2]
20:35:36.478 D/chenhj: Coroutine Main runs after join() (joinBtnClicked) ::running in Thread:[id:2][name:main]

通過日志可以證明關(guān)鍵點(diǎn),雖然只等待一個Job對象完成,事實(shí)上,也會等待到里面A/B/C三個協(xié)程完成。

事實(shí)上,可以將所被記錄下來的Job對象,其實(shí)為父協(xié)程,里面所啟動的A/B/C三個協(xié)程則為子協(xié)程。

協(xié)程完成的條件,概述為一下:

  • 當(dāng)其沒有子協(xié)程時,完成狀態(tài)的條件是自身執(zhí)行結(jié)束;
  • 當(dāng)其有一個或以上的子協(xié)程時,完成狀態(tài)的條件是 自身執(zhí)行結(jié)束 且 所有的子協(xié)程都處于完成狀態(tài);

以上例子可以證明,變量job的最后一行代碼早早已經(jīng)執(zhí)行結(jié)束,但是調(diào)用join()后的log,必然會等待到A/B/C三個子協(xié)程都執(zhí)行結(jié)束輸出最后一行l(wèi)og后才會輸出。

事實(shí)上,join函數(shù)獲得恢復(fù)的時間點(diǎn),是變量job協(xié)程執(zhí)行完成時間點(diǎn)、協(xié)程A執(zhí)行完成時間點(diǎn)、協(xié)程B執(zhí)行完成時間點(diǎn)、協(xié)程C完成時間點(diǎn),這四個時間點(diǎn)中最晚的時間點(diǎn)。

這里,本質(zhì)上便是使用了協(xié)程結(jié)構(gòu)化并發(fā),只要把握住了父協(xié)程去等待,那么父協(xié)程的完成會結(jié)構(gòu)化地等待子協(xié)程完成,這樣,管理維度將不會是一個個獨(dú)立的Job對象,而是利用了結(jié) 構(gòu)化的關(guān)系簡化對Job對象的使用。

3. 進(jìn)階版等待多協(xié)程完成

這里其實(shí)也還是麻煩,因?yàn)檫€是要保存一個Job對象來調(diào)用join函數(shù)。事實(shí)上,還真有進(jìn)一步的優(yōu)化使用。

接下來的主角便是掛起函數(shù)supervisorScope,上代碼:

private fun supervisorScopeBtnClicked() {
    "supervisorScopeBtnClicked".let {
        myLog(it)
    }
    job?.cancel()
    job = scope.launch {
        "Coroutine Main runs (supervisorScopeBtnClicked)".let {
            myLog(it)
        }
        supervisorScope {
            "supervisorScope lambda runs (supervisorScopeBtnClicked)".let {
                myLog(it)
            }
            launch(Dispatchers.IO) {
                testIOCoroutine("supervisorScopeBtnClicked A")
            }
            launch(Dispatchers.IO) {
                testIOCoroutine("supervisorScopeBtnClicked B")
            }
            launch(Dispatchers.IO) {
                testIOCoroutine("supervisorScopeBtnClicked C")
            }
            "supervisorScope lambda runs final statement (supervisorScopeBtnClicked)".let {
                myLog(it)
            }
        }
        "Coroutine Main runs final statement (supervisorScopeBtnClicked)".let {
            myLog(it)
        }
    }
}

好像,這里不也還是保存了啟動的Job對象?先別急,一步步來,現(xiàn)在的關(guān)鍵是,只點(diǎn)擊supervisorScopeBtn而不去點(diǎn)擊joinBtn,產(chǎn)生的log如下:

21:16:08.578 D/chenhj: supervisorScopeBtnClicked ::running in Thread:[id:2][name:main]
21:16:08.625 D/chenhj: Coroutine Main runs (supervisorScopeBtnClicked) ::running in Thread:[id:2][name:main]
21:16:08.628 D/chenhj: supervisorScope lambda runs (supervisorScopeBtnClicked) ::running in Thread:[id:2][name:main]
21:16:08.629 D/chenhj: Coroutine IO runs (supervisorScopeBtnClicked A) ::running in Thread:[id:3036][name:DefaultDispatcher-worker-2]
21:16:08.629 D/chenhj: Coroutine IO runs (supervisorScopeBtnClicked B) ::running in Thread:[id:3037][name:DefaultDispatcher-worker-3]
21:16:08.630 D/chenhj: Coroutine IO runs (supervisorScopeBtnClicked C) ::running in Thread:[id:3035][name:DefaultDispatcher-worker-1]
21:16:08.631 D/chenhj: supervisorScope lambda runs final statement (supervisorScopeBtnClicked) ::running in Thread:[id:2][name:main]
21:16:16.711 D/chenhj: Coroutine IO runs after thread sleep (supervisorScopeBtnClicked C) ::running in Thread:[id:3035][name:DefaultDispatcher-worker-1]
21:16:16.826 D/chenhj: Coroutine IO runs after thread sleep (supervisorScopeBtnClicked B) ::running in Thread:[id:3037][name:DefaultDispatcher-worker-3]
21:16:16.945 D/chenhj: Coroutine IO runs after thread sleep (supervisorScopeBtnClicked A) ::running in Thread:[id:3036][name:DefaultDispatcher-worker-2]
21:16:17.050 D/chenhj: Coroutine Main runs final statement (supervisorScopeBtnClicked) ::running in Thread:[id:2][name:main]

這次的關(guān)鍵點(diǎn)是,在supervisorScope函數(shù)后面的代碼(log輸出),會在A/B/C三個協(xié)程執(zhí)行完成后才會執(zhí)行!

掛起函數(shù)supervisorScope很實(shí)用的一個點(diǎn)便是,會掛起當(dāng)前協(xié)程直到其產(chǎn)生子協(xié)程作用域啟動的所有協(xié)程均執(zhí)行完成后再恢復(fù)當(dāng)前協(xié)程。

說起來是有點(diǎn)繞,不妨自己拿前面的代碼或者demo代碼自行閱讀體會一下。

這里已經(jīng)不需要Job#join()了,直接在supervisorScope后面執(zhí)行的代碼,就已經(jīng)確保了子協(xié)程的執(zhí)行完成!

其實(shí)吧,這里的代碼的細(xì)節(jié)順序還可以再理理,比如哪些是保證了順序的,哪些是不保證順序的。協(xié)程,畢竟是個異步開發(fā)的內(nèi)容,所以代碼執(zhí)行邏輯與順序,很重要!

同樣的,這里也把啟動的job保存了,所以在joinBtn中也可以進(jìn)一步確認(rèn)效果。

4. 樣例工程代碼

代碼樣例Demo,見Github:https://github.com/TeaCChen/CoroutineStudy

本文示例代碼,如覺奇怪或啰嗦,其實(shí)為StructuredStepOneActivity.kt中的代碼摘取主要部分說明,在demo代碼當(dāng)中,為提升細(xì)節(jié)內(nèi)容,有更加多的封裝和輸出內(nèi)容。

本文的頁面截圖示例如下:

image-7-1.png

5. 補(bǔ)充說明

結(jié)構(gòu)化并發(fā),屬于協(xié)程設(shè)計(jì)里的內(nèi)容,本文的內(nèi)容僅是初步地了解。

結(jié)構(gòu)化并發(fā)這個功能是在設(shè)計(jì)上是強(qiáng)大的,比如前面的通過協(xié)程作用域去取消協(xié)程的方式其實(shí)也屬于結(jié)構(gòu)化并發(fā)的內(nèi)容,本文只在等待協(xié)程完成的角度去引出結(jié)構(gòu)化并發(fā)的內(nèi)容,事實(shí)上,結(jié)構(gòu)化并發(fā)的內(nèi)容還有很多,比如異常處理的傳遞、調(diào)度器的傳遞等。

這里必須強(qiáng)調(diào)一個內(nèi)容,協(xié)程之間產(chǎn)生父子關(guān)系的關(guān)鍵是協(xié)程作用域(更根本上說,是協(xié)程作用域中的協(xié)程上下文的Job對象),而并不是啟動協(xié)程的層級!

協(xié)程的內(nèi)容是豐富而強(qiáng)大的,學(xué)習(xí)和使用的過程中不要指望一蹴而就。

一學(xué)就會的協(xié)程使用——基礎(chǔ)篇

一學(xué)就會的協(xié)程使用——基礎(chǔ)篇(一)協(xié)程啟動

一學(xué)就會的協(xié)程使用——基礎(chǔ)篇(二)線程切換

一學(xué)就會的協(xié)程使用——基礎(chǔ)篇(三)初遇協(xié)程取消

一學(xué)就會的協(xié)程使用——基礎(chǔ)篇(四)協(xié)程作用域

一學(xué)就會的協(xié)程使用——基礎(chǔ)篇(五)再遇協(xié)程取消

一學(xué)就會的協(xié)程使用——基礎(chǔ)篇(六)初識掛起

一學(xué)就會的協(xié)程使用——基礎(chǔ)篇(七)初識結(jié)構(gòu)化(本文)

一學(xué)就會的協(xié)程使用——基礎(chǔ)篇(八)初識協(xié)程異常

一學(xué)就會的協(xié)程使用——基礎(chǔ)篇(九)異常與supervisor

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

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

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