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

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)容们颜。

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

image-7-1.png

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ǔ)篇

一學(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)系作者
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者眠屎。
  • 序言:七十年代末剔交,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子改衩,更是在濱河造成了極大的恐慌岖常,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件葫督,死亡現(xiàn)場離奇詭異竭鞍,居然都是意外死亡,警方通過查閱死者的電腦和手機橄镜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門偎快,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人洽胶,你說我怎么就攤上這事晒夹。” “怎么了?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵丐怯,是天一觀的道長喷好。 經(jīng)常有香客問我,道長响逢,這世上最難降的妖魔是什么绒窑? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任棕孙,我火速辦了婚禮舔亭,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蟀俊。我一直安慰自己钦铺,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布肢预。 她就那樣靜靜地躺著矛洞,像睡著了一般。 火紅的嫁衣襯著肌膚如雪烫映。 梳的紋絲不亂的頭發(fā)上沼本,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天,我揣著相機與錄音锭沟,去河邊找鬼抽兆。 笑死,一個胖子當(dāng)著我的面吹牛族淮,可吹牛的內(nèi)容都是我干的辫红。 我是一名探鬼主播,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼祝辣,長吁一口氣:“原來是場噩夢啊……” “哼贴妻!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蝙斜,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤名惩,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后孕荠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體娩鹉,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年岛琼,在試婚紗的時候發(fā)現(xiàn)自己被綠了底循。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡槐瑞,死狀恐怖熙涤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤祠挫,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布那槽,位于F島的核電站,受9級特大地震影響等舔,放射性物質(zhì)發(fā)生泄漏骚灸。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一慌植、第九天 我趴在偏房一處隱蔽的房頂上張望甚牲。 院中可真熱鬧,春花似錦蝶柿、人聲如沸丈钙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽雏赦。三九已至,卻和暖如春芙扎,著一層夾襖步出監(jiān)牢的瞬間星岗,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工戒洼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留俏橘,地道東北人。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓施逾,卻偏偏與公主長得像敷矫,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子汉额,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,781評論 2 354

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