1. 引言
本文主要是通過比較實用的掛起函數(shù)join和await來接觸實踐協(xié)程的掛起作用,同時本部分將會有較多的理解內(nèi)容。
2. 等待協(xié)程執(zhí)行完成
不多說,直接上代碼!
某啟動一個協(xié)程并將job對象保存下來:
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)
}
}
}
然后另外一個地方,等待這個協(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,請問下面這兩行l(wèi)og,輸出的順序會是?
"Coroutine IO runs after thread sleep (from launchBtn)"
"Coroutine Main runs after join() (from joinBtn)"
事實上,這兩行的log的輸出順序,必然是先第一行再第二行!
這便是由于掛起函數(shù)join的作用產(chǎn)生的效果!
掛起函數(shù)
join的作用:掛起調(diào)用處所在的協(xié)程直到調(diào)用者協(xié)程執(zhí)行完成。
3. 協(xié)程與線程等待完成函數(shù)的對照
協(xié)程中Job的join函數(shù)與線程Thread的join函數(shù)在功能設(shè)計上其實是類似的。
線程/協(xié)程對象的join函數(shù)調(diào)用后,將在調(diào)用處等待線程/協(xié)程對象執(zhí)行完成后再繼續(xù)往下執(zhí)行。
好像比較籠統(tǒng)或不好理解?那么來個詳細(xì)對比版吧:
在線程A執(zhí)行過程中調(diào)用了線程B的join函數(shù),那么線程A進(jìn)入阻塞狀態(tài)(BLOCKED),直到線程B執(zhí)行完成后再轉(zhuǎn)化為可執(zhí)行狀態(tài)(RUNNABLE),線程A在獲得CPU時間片后再繼續(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í)行。
這里盡量簡潔了,如果還是看不懂?……那就……多看幾遍?如果還是不懂?…………罷了罷了,不懂的話,建議先記下吧。
4. 關(guān)于掛起不得不提的點(diǎn)
說到協(xié)程的掛起,必要強(qiáng)調(diào)以下的核心內(nèi)容:
1) 操作系統(tǒng)層面沒有協(xié)程的存在;
2) 協(xié)程的掛起狀態(tài)不對應(yīng)任何的線程狀態(tài);
3) 協(xié)程處于掛起狀態(tài)之時,不占用或阻塞任何線程;
4) 如果用的是runBlocking方式啟動協(xié)程,上面的第2和第3點(diǎn)將不再成立;
對于第2和第3點(diǎn),這便是協(xié)程掛起的神奇之處!
掛起函數(shù)的調(diào)用,雖然在邏輯上是依次執(zhí)行的,但是從操作系統(tǒng)執(zhí)行字節(jié)碼角度來看,掛起函數(shù)的執(zhí)行過程卻會是異步回調(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)容不是想勸退?”
“對不起咯,實在沒忍住,見諒見諒?!?/p>
個人覺得,說到協(xié)程的掛起,這些內(nèi)容還是必須要提的,理解好不理解也罷,起碼得有個印象,協(xié)程的掛起畢竟是非常核心且關(guān)鍵的內(nèi)容。
5. 獲得協(xié)程的執(zhí)行結(jié)果返回
應(yīng)該都知道,launch方式啟動的協(xié)程沒有帶有返回值,而async方式啟動的協(xié)程可以帶有返回值。
可能有不知道的小伙伴?我不管,反正你現(xiàn)在知道了。
或許有小伙伴經(jīng)不住會問,"啥玩意?launch函數(shù)不是明明有返回值Job嗎?為啥說沒有返回值呢?“
好吧,這部分其實是函數(shù)式編程設(shè)計的內(nèi)容,我說的是協(xié)程帶有返回值,說的是協(xié)程執(zhí)行體(一般寫法會是lambda表達(dá)式的函數(shù)體部分)的返回值,而不是launch函數(shù)的返回值。
如果這個沒搞懂,建議先學(xué)習(xí)了解下Kotlin的函數(shù)類型、lambda表達(dá)式等函數(shù)式編程設(shè)計內(nèi)容。
…………怎么感覺不大對?隱約間又說道別的內(nèi)容了?好吧,沒忍住。
趕緊上代碼!
先是通過async啟動協(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獲取所啟動協(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,那么下面兩行的日志輸出將會始終保證順序:
"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將會是async啟動的協(xié)程函數(shù)體里的返回值,當(dāng)前實踐代碼中,類型是String,值為"TeaC"。
協(xié)程函數(shù)體的返回值?協(xié)程函數(shù)體里沒看到有返回值的返回?。亢冒?,這里搞清楚一個點(diǎn),async后的花括號部分其實是lambda表達(dá)式,而lambda表達(dá)式函數(shù)體部分的返回值會是最后一個表達(dá)式的返回值,可以有顯式的return關(guān)鍵字方式,但是Kotlin開發(fā)文檔中并不建議顯式寫出return這種方式……
好像有點(diǎn)不對?打住打?。∵@部分其實是Kotlin函數(shù)式編程內(nèi)容,所以…………
回到上述代碼,其實便是通過掛起函數(shù)await,獲得了async所啟動的協(xié)程函數(shù)體中的返回值。如目標(biāo)協(xié)程還未結(jié)束時,將掛起等待最終結(jié)果的返回。
6. 兩種協(xié)程啟動方式的對比
兩種協(xié)程啟動方式,分別指的是launch和async啟動協(xié)程的方式對比。
更具體地說,應(yīng)該是(launch/Job/join)和(async/Deferred/await)這兩個組合拳之間的對比。
- launch函數(shù)的返回值是Job,而async函數(shù)的返回值是Deferred<T>;
- launch啟動的協(xié)程函數(shù)體的返回值必然是Unit,而async啟動的協(xié)程函數(shù)體的返回值將是最后一個表達(dá)式的值;
- Job#join()和Deferred#await()均是掛起函數(shù),都有掛起協(xié)程等待協(xié)程執(zhí)行完成的作用,但是前者沒有返回值(又或說返回值是Unit),后者有返回值,返回值將是async的協(xié)程函數(shù)體中的返回值;
事實上,兩者對比上的差異遠(yuǎn)不止上述內(nèi)容,比如在協(xié)程不同條件下的取消表現(xiàn),關(guān)于join/await總結(jié)如下:
對于join函數(shù)在各種場景下的總結(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)用不會產(chǎn)生實際性效果且會繼續(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ù)之處會拋出CancellationException;
對于await函數(shù)在各種場景下的總結(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方法處會拋出CancellationException;
不用擔(dān)心異常CancellationException的拋出,在協(xié)程函數(shù)體和掛起函數(shù)執(zhí)行中,異常CancellationException是用作協(xié)程取消協(xié)作點(diǎn)用的,前文的取消篇內(nèi)容所用的ensureActive函數(shù)的真正取消協(xié)作點(diǎn)也是拋出此種異常。
注:完整的實踐代碼中,也提供了協(xié)程取消的寫法,根據(jù)已有的代碼作進(jìn)一步修改,可以實踐驗證上面的總結(jié)。
7. 樣例工程代碼
代碼樣例Demo,見Github:https://github.com/TeaCChen/CoroutineStudy
本文示例代碼,如覺奇怪或啰嗦,其實為CancelStepTwoActivity.kt中的代碼摘取主要部分說明,在demo代碼當(dāng)中,為提升細(xì)節(jié)內(nèi)容,有更加多的封裝和輸出內(nèi)容。
本文的頁面截圖示例如下:

一學(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)化