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)容。
本文的頁面截圖示例如下:

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ǔ)篇(一)協(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)化(本文)