一學(xué)就會的協(xié)程使用——基礎(chǔ)篇(六)初遇掛起

1. 引言

本文主要是通過比較實用的掛起函數(shù)joinawait來接觸實踐協(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)容。

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

image-6-1.png

一學(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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容