1. 引言
前文提及了join
函數(shù)寨躁,那么進一步的便是協(xié)程中非常強大的結(jié)構(gòu)化并發(fā)設(shè)計了穆碎。
結(jié)構(gòu)化并發(fā)(structured concurrency)從字面上并不直觀能理解,本文將通過實踐代碼來介紹职恳。
2. 巧妙地等待協(xié)程完成
前面有所介紹所禀,希望等待某一個協(xié)程執(zhí)行完成時,可以使用Job#join()
放钦,那么自然而言地北秽,如果需要等個多個協(xié)程,那么就將每次啟動的協(xié)程Job對象收集或保存起來再逐個調(diào)用即可最筒。
嗯贺氓,這樣的確可以在功能上達到目的。但是床蜘,想一下辙培,在協(xié)程取消剛開始時我們是從Job維度去實現(xiàn)取消的,這當(dāng)然可以實現(xiàn)取消的需要邢锯,但是在進行到協(xié)程作用域部分的實踐時扬蕊,便是不再通過Job的維度去取消,而是通過協(xié)程作用域的維度丹擎。那么尾抑,可以想想,在等待協(xié)程完成中是否也有這種功能呢蒂培?
事實上再愈,卻是可以有更好的選擇,那便是結(jié)構(gòu)化并發(fā)护戳。
上代碼前翎冲,假定一個場景吧:某一時刻要進行三個耗時的操作,三個操作之間互不干擾媳荒,三個操作都執(zhí)行完成后再進行下一步執(zhí)行抗悍。
這里假定的耗時操作實踐代碼如下:
private suspend fun testIOCoroutine(calledMsg: String) {
"Coroutine IO runs ($calledMsg)".let {
myLog(it)
}
/* 僅為樣例代碼驹饺,休眠線程其實是非常非常不建議的做法!缴渊! */
Thread.sleep(randomMilli)
"Coroutine IO runs after thread sleep ($calledMsg)".let {
myLog(it)
}
}
本質(zhì)上是以將當(dāng)前線程休眠一個隨機的時間赏壹,為了使時間代碼更有趣些,每次調(diào)用時休眠的時間這里其實是5000毫秒到10000毫秒之間的隨機值:
val randomMilli: Long
get() = (FIVE_SECONDS..TEN_SECONDS).random()
接下來按照假定的場景衔沼,分別啟動三個協(xié)程進行三次耗時操作調(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)
}
}
}
先點擊launchStructuredBtn再點擊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)鍵點劣光,雖然只等待一個Job對象完成,事實上糟把,也會等待到里面A/B/C三個協(xié)程完成绢涡。
事實上,可以將所被記錄下來的Job對象遣疯,其實為父協(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后才會輸出。
事實上燎悍,join
函數(shù)獲得恢復(fù)的時間點敬惦,是變量job協(xié)程執(zhí)行完成時間點、協(xié)程A執(zhí)行完成時間點间涵、協(xié)程B執(zhí)行完成時間點仁热、協(xié)程C完成時間點,這四個時間點中最晚的時間點勾哩。
這里抗蠢,本質(zhì)上便是使用了協(xié)程結(jié)構(gòu)化并發(fā),只要把握住了父協(xié)程去等待思劳,那么父協(xié)程的完成會結(jié)構(gòu)化地等待子協(xié)程完成迅矛,這樣,管理維度將不會是一個個獨立的Job對象潜叛,而是利用了結(jié) 構(gòu)化的關(guān)系簡化對Job對象的使用秽褒。
3. 進階版等待多協(xié)程完成
這里其實也還是麻煩,因為還是要保存一個Job對象來調(diào)用join函數(shù)威兜。事實上销斟,還真有進一步的優(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)鍵是,只點擊supervisorScopeBtn而不去點擊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)鍵點是涝动,在supervisorScope
函數(shù)后面的代碼(log輸出),會在A/B/C三個協(xié)程執(zhí)行完成后才會執(zhí)行炬灭!
掛起函數(shù)supervisorScope
很實用的一個點便是醋粟,會掛起當(dāng)前協(xié)程直到其產(chǎn)生子協(xié)程作用域啟動的所有協(xié)程均執(zhí)行完成后再恢復(fù)當(dāng)前協(xié)程。
說起來是有點繞重归,不妨自己拿前面的代碼或者demo代碼自行閱讀體會一下米愿。
這里已經(jīng)不需要Job#join()了,直接在supervisorScope
后面執(zhí)行的代碼提前,就已經(jīng)確保了子協(xié)程的執(zhí)行完成吗货!
其實吧,這里的代碼的細節(jié)順序還可以再理理狈网,比如哪些是保證了順序的宙搬,哪些是不保證順序的。協(xié)程拓哺,畢竟是個異步開發(fā)的內(nèi)容勇垛,所以代碼執(zhí)行邏輯與順序,很重要士鸥!
同樣的闲孤,這里也把啟動的job保存了,所以在joinBtn中也可以進一步確認效果烤礁。
4. 樣例工程代碼
代碼樣例Demo讼积,見Github:https://github.com/TeaCChen/CoroutineStudy
本文示例代碼肥照,如覺奇怪或啰嗦,其實為StructuredStepOneActivity.kt
中的代碼摘取主要部分說明勤众,在demo代碼當(dāng)中舆绎,為提升細節(jié)內(nèi)容,有更加多的封裝和輸出內(nèi)容们颜。
本文的頁面截圖示例如下:
5. 補充說明
結(jié)構(gòu)化并發(fā)吕朵,屬于協(xié)程設(shè)計里的內(nèi)容,本文的內(nèi)容僅是初步地了解窥突。
結(jié)構(gòu)化并發(fā)這個功能是在設(shè)計上是強大的努溃,比如前面的通過協(xié)程作用域去取消協(xié)程的方式其實也屬于結(jié)構(gòu)化并發(fā)的內(nèi)容,本文只在等待協(xié)程完成的角度去引出結(jié)構(gòu)化并發(fā)的內(nèi)容阻问,事實上梧税,結(jié)構(gòu)化并發(fā)的內(nèi)容還有很多,比如異常處理的傳遞则拷、調(diào)度器的傳遞等贡蓖。
這里必須強調(diào)一個內(nèi)容,協(xié)程之間產(chǎn)生父子關(guān)系的關(guān)鍵是協(xié)程作用域(更根本上說煌茬,是協(xié)程作用域中的協(xié)程上下文的Job對象)斥铺,而并不是啟動協(xié)程的層級!
協(xié)程的內(nèi)容是豐富而強大的坛善,學(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)化(本文)