1. 引言
本文主要是通過比較實(shí)用的掛起函數(shù)join
和await
來接觸實(shí)踐協(xié)程的掛起作用凿掂,同時(shí)本部分將會(huì)有較多的理解內(nèi)容涣觉。
2. 等待協(xié)程執(zhí)行完成
不多說,直接上代碼烘豌!
某啟動(dòng)一個(gè)協(xié)程并將job對(duì)象保存下來:
viewBinding.launchBtn -> {
"Clicked launchBtn".let {
myLog(it)
}
job?.cancel()
job = scope.launch(Dispatchers.IO) {
"Coroutine IO runs (from launchBtn)".let {
myLog(it)
}
Thread.sleep(FIVE_SECONDS)
"Coroutine IO runs after thread sleep (from launchBtn)".let {
myLog(it)
}
}
}
然后另外一個(gè)地方,等待這個(gè)協(xié)程的執(zhí)行結(jié)束看彼,這里關(guān)鍵是join
函數(shù)廊佩!
viewBinding.joinBtn -> {
"Clicked joinBtn".let {
myLog(it)
}
scope.launch(Dispatchers.Main) {
"Coroutine Main runs (from joinBtn)".let {
myLog(it)
}
val jobNonNull = job ?: throw IllegalStateException("No job launched yet!")
jobNonNull.join()
"Coroutine Main runs after join() (from joinBtn)".let {
myLog(it)
}
}
}
這樣的話,先點(diǎn)擊launchBtn后在5秒內(nèi)點(diǎn)擊joinBtn靖榕,請(qǐng)問下面這兩行l(wèi)og标锄,輸出的順序會(huì)是?
"Coroutine IO runs after thread sleep (from launchBtn)"
"Coroutine Main runs after join() (from joinBtn)"
事實(shí)上茁计,這兩行的log的輸出順序料皇,必然是先第一行再第二行!
這便是由于掛起函數(shù)join
的作用產(chǎn)生的效果星压!
掛起函數(shù)
join
的作用:掛起調(diào)用處所在的協(xié)程直到調(diào)用者協(xié)程執(zhí)行完成践剂。
3. 協(xié)程與線程等待完成函數(shù)的對(duì)照
協(xié)程中Job的join
函數(shù)與線程Thread的join
函數(shù)在功能設(shè)計(jì)上其實(shí)是類似的。
線程/協(xié)程對(duì)象的join函數(shù)調(diào)用后娜膘,將在調(diào)用處等待線程/協(xié)程對(duì)象執(zhí)行完成后再繼續(xù)往下執(zhí)行逊脯。
好像比較籠統(tǒng)或不好理解?那么來個(gè)詳細(xì)對(duì)比版吧:
在線程A執(zhí)行過程中調(diào)用了線程B的join函數(shù)竣贪,那么線程A進(jìn)入阻塞狀態(tài)(BLOCKED)军洼,直到線程B執(zhí)行完成后再轉(zhuǎn)化為可執(zhí)行狀態(tài)(RUNNABLE)巩螃,線程A在獲得CPU時(shí)間片后再繼續(xù)往下執(zhí)行。
在協(xié)程C執(zhí)行過程中調(diào)用了協(xié)程D的join函數(shù)匕争,那么協(xié)程C進(jìn)入掛起狀態(tài)(SUSPENDED)牺六,直到協(xié)程D執(zhí)行完成后再轉(zhuǎn)換為恢復(fù)狀態(tài)(RESUMED),協(xié)程C在獲得調(diào)度器的調(diào)度后再繼續(xù)往下執(zhí)行汗捡。
這里盡量簡(jiǎn)潔了淑际,如果還是看不懂?……那就……多看幾遍扇住?如果還是不懂春缕?…………罷了罷了,不懂的話艘蹋,建議先記下吧锄贼。
4. 關(guān)于掛起不得不提的點(diǎn)
說到協(xié)程的掛起,必要強(qiáng)調(diào)以下的核心內(nèi)容:
1) 操作系統(tǒng)層面沒有協(xié)程的存在女阀;
2) 協(xié)程的掛起狀態(tài)不對(duì)應(yīng)任何的線程狀態(tài)宅荤;
3) 協(xié)程處于掛起狀態(tài)之時(shí),不占用或阻塞任何線程浸策;
4) 如果用的是runBlocking
方式啟動(dòng)協(xié)程冯键,上面的第2和第3點(diǎn)將不再成立;
對(duì)于第2和第3點(diǎn)庸汗,這便是協(xié)程掛起的神奇之處惫确!
掛起函數(shù)的調(diào)用,雖然在邏輯上是依次執(zhí)行的蚯舱,但是從操作系統(tǒng)執(zhí)行字節(jié)碼角度來看改化,掛起函數(shù)的執(zhí)行過程卻會(huì)是異步回調(diào)式的執(zhí)行邏輯。
點(diǎn)到即止枉昏,這部分是協(xié)程掛起中非常核心的內(nèi)容:CPS轉(zhuǎn)換和狀態(tài)機(jī)陈肛,有興趣的可以拓展深入探究或?qū)W習(xí)。
這里是基礎(chǔ)學(xué)習(xí)篇……
“哼兄裂,虧你還知道是基礎(chǔ)學(xué)習(xí)篇句旱,還放出這么多理解的內(nèi)容不是想勸退?”
“對(duì)不起咯懦窘,實(shí)在沒忍住前翎,見諒見諒稚配〕┩浚”
個(gè)人覺得,說到協(xié)程的掛起道川,這些內(nèi)容還是必須要提的午衰,理解好不理解也罷立宜,起碼得有個(gè)印象,協(xié)程的掛起畢竟是非常核心且關(guān)鍵的內(nèi)容臊岸。
5. 獲得協(xié)程的執(zhí)行結(jié)果返回
應(yīng)該都知道橙数,launch
方式啟動(dòng)的協(xié)程沒有帶有返回值,而async
方式啟動(dòng)的協(xié)程可以帶有返回值帅戒。
可能有不知道的小伙伴灯帮?我不管,反正你現(xiàn)在知道了逻住。
或許有小伙伴經(jīng)不住會(huì)問钟哥,"啥玩意?launch函數(shù)不是明明有返回值Job嗎瞎访?為啥說沒有返回值呢腻贰?“
好吧,這部分其實(shí)是函數(shù)式編程設(shè)計(jì)的內(nèi)容扒秸,我說的是協(xié)程帶有返回值播演,說的是協(xié)程執(zhí)行體(一般寫法會(huì)是lambda表達(dá)式的函數(shù)體部分)的返回值,而不是launch函數(shù)的返回值。
如果這個(gè)沒搞懂袍镀,建議先學(xué)習(xí)了解下Kotlin的函數(shù)類型挽荡、lambda表達(dá)式等函數(shù)式編程設(shè)計(jì)內(nèi)容。
…………怎么感覺不大對(duì)顶霞?隱約間又說道別的內(nèi)容了?好吧锣吼,沒忍住选浑。
趕緊上代碼!
先是通過async
啟動(dòng)協(xié)程部分:
viewBinding.asyncBtn -> {
"Clicked asyncBtn".let {
myLog(it)
}
deferred?.cancel()
deferred = scope.async(Dispatchers.IO) {
val stringBuilder = StringBuilder()
"Coroutine IO runs (from asyncBtn)".let {
myLog(it)
}
Thread.sleep(FIVE_SECONDS)
"TeaC".apply {
"Coroutine IO runs after thread sleep: $this (from asyncBtn)".let {
myLog(it)
}
}
}
}
再是通過掛起函數(shù)await
獲取所啟動(dòng)協(xié)程的返回值部分:
viewBinding.awaitBtn -> {
"Clicked awaitBtn".let {
myLog(it)
}
scope.launch(Dispatchers.Main) {
"Coroutine Main runs (from awaitBtn)".let {
myLog(it)
}
val deferredNonNull =
deferred ?: throw IllegalStateException("No deferred async yet!")
val ret = deferredNonNull.await()
"Coroutine Main runs after await(): $ret (from awaitBtn)".let {
myLog(it)
}
}
}
同樣的玄叠,先點(diǎn)擊asyncBtn然后5秒內(nèi)點(diǎn)擊awaitBtn古徒,那么下面兩行的日志輸出將會(huì)始終保證順序:
"Coroutine IO runs after thread sleep: $this (from asyncBtn)"
"Coroutine Main runs after await(): TeaC (from awaitBtn)"
與join
不同的是,await
是有返回值的读恃,注意關(guān)鍵代碼:
val ret = deferredNonNull.await()
上述代碼隧膘,這里ret將會(huì)是async
啟動(dòng)的協(xié)程函數(shù)體里的返回值,當(dāng)前實(shí)踐代碼中寺惫,類型是String疹吃,值為"TeaC"。
協(xié)程函數(shù)體的返回值西雀?協(xié)程函數(shù)體里沒看到有返回值的返回叭弧?好吧艇肴,這里搞清楚一個(gè)點(diǎn)腔呜,async
后的花括號(hào)部分其實(shí)是lambda表達(dá)式叁温,而lambda表達(dá)式函數(shù)體部分的返回值會(huì)是最后一個(gè)表達(dá)式的返回值,可以有顯式的return關(guān)鍵字方式核畴,但是Kotlin開發(fā)文檔中并不建議顯式寫出return這種方式……
好像有點(diǎn)不對(duì)膝但?打住打住谤草!這部分其實(shí)是Kotlin函數(shù)式編程內(nèi)容跟束,所以…………
回到上述代碼,其實(shí)便是通過掛起函數(shù)await
丑孩,獲得了async
所啟動(dòng)的協(xié)程函數(shù)體中的返回值泳炉。如目標(biāo)協(xié)程還未結(jié)束時(shí),將掛起等待最終結(jié)果的返回嚎杨。
6. 兩種協(xié)程啟動(dòng)方式的對(duì)比
兩種協(xié)程啟動(dòng)方式花鹅,分別指的是launch和async啟動(dòng)協(xié)程的方式對(duì)比。
更具體地說枫浙,應(yīng)該是(launch/Job/join)和(async/Deferred/await)這兩個(gè)組合拳之間的對(duì)比刨肃。
- launch函數(shù)的返回值是Job,而async函數(shù)的返回值是Deferred<T>箩帚;
- launch啟動(dòng)的協(xié)程函數(shù)體的返回值必然是Unit真友,而async啟動(dòng)的協(xié)程函數(shù)體的返回值將是最后一個(gè)表達(dá)式的值;
- Job#join()和Deferred#await()均是掛起函數(shù)紧帕,都有掛起協(xié)程等待協(xié)程執(zhí)行完成的作用盔然,但是前者沒有返回值(又或說返回值是Unit),后者有返回值是嗜,返回值將是async的協(xié)程函數(shù)體中的返回值愈案;
事實(shí)上,兩者對(duì)比上的差異遠(yuǎn)不止上述內(nèi)容鹅搪,比如在協(xié)程不同條件下的取消表現(xiàn)站绪,關(guān)于join/await總結(jié)如下:
對(duì)于join
函數(shù)在各種場(chǎng)景下的總結(jié):
1)協(xié)程B中調(diào)用了協(xié)程A的join函數(shù)后,協(xié)程B等待到協(xié)程A完成后才繼續(xù)往下執(zhí)行丽柿;
2)協(xié)程B在等待協(xié)程A完成的過程中恢准,協(xié)程掛起,但協(xié)程B所執(zhí)行在的線程并沒有阻塞甫题;
3)協(xié)程B在調(diào)用協(xié)程A的join函數(shù)前馁筐,協(xié)程A已經(jīng)完成,則join函數(shù)被調(diào)用不會(huì)產(chǎn)生實(shí)際性效果且會(huì)繼續(xù)下執(zhí)行坠非;
4)協(xié)程B在掛起等待協(xié)程A的過程中敏沉,如果協(xié)程A被取消,則協(xié)程B的掛起狀態(tài)結(jié)束且繼續(xù)正常往下執(zhí)行;
5)協(xié)程B在掛起等待協(xié)程A的過程中赦抖,如果協(xié)程B被取消舱卡,則協(xié)程B在調(diào)用join函數(shù)之處會(huì)拋出CancellationException辅肾;
對(duì)于await
函數(shù)在各種場(chǎng)景下的總結(jié):
1)協(xié)程B中調(diào)用了協(xié)程A的await函數(shù)后队萤,協(xié)程B等待到協(xié)程A完成并返回結(jié)果后才繼續(xù)往下執(zhí)行;
2)協(xié)程B在等待協(xié)程A結(jié)果的過程中矫钓,協(xié)程掛起要尔,但協(xié)程B所執(zhí)行在的線程并沒有阻塞;
3)協(xié)程B在調(diào)用協(xié)程A的await函數(shù)前新娜,協(xié)程A已經(jīng)完成并返回結(jié)果赵辕,則await函數(shù)直接返回協(xié)程A的執(zhí)行結(jié)果且往下繼續(xù)執(zhí)行;
4)協(xié)程B在掛起等待協(xié)程A結(jié)果的過程中概龄,如果協(xié)程A被取消还惠,則協(xié)程B在調(diào)用協(xié)程A的await方法處拋出CancellationException;
5)協(xié)程B在掛起等待協(xié)程A結(jié)果的過程中私杜,如果協(xié)程B被取消蚕键,則協(xié)程B在調(diào)用協(xié)程A的await方法處會(huì)拋出CancellationException;
不用擔(dān)心異常CancellationException的拋出衰粹,在協(xié)程函數(shù)體和掛起函數(shù)執(zhí)行中锣光,異常CancellationException是用作協(xié)程取消協(xié)作點(diǎn)用的,前文的取消篇內(nèi)容所用的ensureActive
函數(shù)的真正取消協(xié)作點(diǎn)也是拋出此種異常铝耻。
注:完整的實(shí)踐代碼中誊爹,也提供了協(xié)程取消的寫法,根據(jù)已有的代碼作進(jìn)一步修改瓢捉,可以實(shí)踐驗(yàn)證上面的總結(jié)频丘。
7. 樣例工程代碼
代碼樣例Demo,見Github:https://github.com/TeaCChen/CoroutineStudy
本文示例代碼泡态,如覺奇怪或啰嗦椎镣,其實(shí)為CancelStepTwoActivity.kt
中的代碼摘取主要部分說明,在demo代碼當(dāng)中兽赁,為提升細(xì)節(jié)內(nèi)容状答,有更加多的封裝和輸出內(nèi)容。
本文的頁面截圖示例如下:
一學(xué)就會(huì)的協(xié)程使用——基礎(chǔ)篇
一學(xué)就會(huì)的協(xié)程使用——基礎(chǔ)篇(一)協(xié)程啟動(dòng)
一學(xué)就會(huì)的協(xié)程使用——基礎(chǔ)篇(二)線程切換
一學(xué)就會(huì)的協(xié)程使用——基礎(chǔ)篇(三)初遇協(xié)程取消
一學(xué)就會(huì)的協(xié)程使用——基礎(chǔ)篇(四)協(xié)程作用域
一學(xué)就會(huì)的協(xié)程使用——基礎(chǔ)篇(五)再遇協(xié)程取消
一學(xué)就會(huì)的協(xié)程使用——基礎(chǔ)篇(六)初識(shí)掛起(本文)
一學(xué)就會(huì)的協(xié)程使用——基礎(chǔ)篇(七)初識(shí)結(jié)構(gòu)化