kotlin協(xié)程[8]:再說作用域

CoroutineScope:

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

定義新協(xié)程的范圍。每個(gè)協(xié)程構(gòu)建器都是CoroutineScope的擴(kuò)展,并繼承其coroutineContext以自動(dòng)傳播上下文元素和取消。

獲取范圍的獨(dú)立實(shí)例的最佳方法是CoroutineScope()和MainScope()工廠函數(shù)??梢允褂胮lus運(yùn)算符將其他上下文元素附加到作用域。

建議不要手動(dòng)實(shí)現(xiàn)此接口,應(yīng)優(yōu)先考慮通過委派實(shí)現(xiàn)。按照慣例,作用域的上下文應(yīng)包含作業(yè)實(shí)例以強(qiáng)制執(zhí)行結(jié)構(gòu)化并發(fā)。

每個(gè)協(xié)同程序構(gòu)建器(如launch,async等)和每個(gè)作用域函數(shù)(如coroutineScope,withContext等)都會(huì)將自己的作用域?qū)嵗峁┙o它運(yùn)行的內(nèi)部代碼塊。按照慣例,它們都會(huì)等待塊內(nèi)的所有協(xié)同程序在完成自己之前完成,從而強(qiáng)制執(zhí)行結(jié)構(gòu)化并發(fā)規(guī)則。

CoroutineScope應(yīng)該在具有明確定義的生命周期的實(shí)體上實(shí)現(xiàn)(或用作字段),這些實(shí)體負(fù)責(zé)啟動(dòng)子協(xié)同程序

https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html

CoroutineScope是必須的么?其實(shí)不是的。當(dāng)協(xié)程還是實(shí)驗(yàn)性質(zhì)的時(shí)候Kotlin 1.1時(shí),我們啟動(dòng)協(xié)程是可以這樣寫的:

fun requestSomeData() {
    launch {
        updateUI(performRequest())
    }
}

這里我們?cè)赨I上下文中啟動(dòng)一個(gè)新的協(xié)同程序launch(UI),調(diào)用掛起函數(shù)performRequest對(duì)后端進(jìn)行異步調(diào)用而不阻塞主UI線程,然后用結(jié)果更新UI。每個(gè)requestSomeData調(diào)用創(chuàng)建自己的協(xié)程,它很好,不是嗎?

但這是一個(gè)問題。如果網(wǎng)絡(luò)或后端出現(xiàn)問題,這些異步操作可能需要很長時(shí)間才能完成。而且,這些操作通常在某些UI元素(如窗口或頁面)的范圍內(nèi)執(zhí)行。如果操作需要很長時(shí)間才能完成,則典型用戶會(huì)關(guān)閉相應(yīng)的UI元素并執(zhí)行其他操作,或者更糟糕的是,重新打開此UI并一次又一次地嘗試操作。但是我們之前的操作仍然在后臺(tái)運(yùn)行,當(dāng)用戶關(guān)閉相應(yīng)的UI元素時(shí),我們需要一些機(jī)制來取消它。

一個(gè)簡單的launch { … }易于編寫,但它不是你應(yīng)該寫的。

協(xié)同程序始終與應(yīng)用程序中的某些本地作用域相關(guān),這是一個(gè)生命周期有限的實(shí)體,如UI元素。因此,對(duì)于結(jié)構(gòu)化并發(fā),我們現(xiàn)在要求在CoroutineScope中調(diào)用啟動(dòng),CoroutineScope是由您的終身受限對(duì)象(如UI元素或其對(duì)應(yīng)的視圖模型)實(shí)現(xiàn)的接口。

對(duì)于更新UI操作CoroutineScope提供專門的實(shí)現(xiàn),在這里可以看到

對(duì)于那些需要全局協(xié)程,其生命周期受應(yīng)用程序生命周期限制的極少數(shù)情況,我們現(xiàn)在提供GlobalScope對(duì)象,因此之前為全局協(xié)程啟動(dòng)launch{...},現(xiàn)在變?yōu)?code>GlobalScope.launch {...},這個(gè)協(xié)同程序的全局特性在代碼中變得明確。GlobalScope在之前的幾章中經(jīng)常用到的。

emmm............加入CoroutineScope就只是解決了這個(gè)異步操作的問題么?

再看下面示例:

suspend fun loadAndCombine(name1: String, name2: String): Image { 
    val deferred1 = async { loadImage(name1) }
    val deferred2 = async { loadImage(name2) }
    return combineImages(deferred1.await(), deferred2.await())
}

這個(gè)例子看起來不錯(cuò),這個(gè)suspend函數(shù)最終會(huì)在某個(gè)協(xié)程內(nèi)部調(diào)用,異步下載2張圖片然后合并成一張,但是還是有很多微妙的錯(cuò)誤,如果這個(gè)協(xié)程取消怎么辦?然后加載兩個(gè)圖片的異步任務(wù)仍然沒有受到影響,這不是一個(gè)可靠的代碼。

那在父協(xié)程取消的時(shí)候把子協(xié)程都取消不就可以了,改成這樣async(coroutineContext) { … }。

它仍然還是有問題,比如下載第一張圖片失敗了,則deferred1.await()拋出了相應(yīng)的異常,但是加載第二張圖片的協(xié)程仍然在后臺(tái)工作,解決這個(gè)問題就更加復(fù)雜了。

一個(gè)簡單async { … }易于編寫,但它不是你應(yīng)該寫的。

使用結(jié)構(gòu)化并發(fā)async協(xié)同程序構(gòu)建器CoroutineScope就像是一樣成為擴(kuò)展launch。你不能簡單地寫async { … },你必須提供范圍。一個(gè)適當(dāng)?shù)牟⑿蟹纸獾睦幼兂桑?/p>

suspend fun loadAndCombine(name1: String, name2: String): Image =
    coroutineScope { 
        val deferred1 = async { loadImage(name1) }
        val deferred2 = async { loadImage(name2) }
        combineImages(deferred1.await(), deferred2.await())
}

你必須將代碼包裝到coroutineScope { … }塊中,以建立操作的邊界及其范圍。所有async協(xié)同程序都成為此范圍的子代,如果范圍因異常而失敗或被取消,則所有子代也將被取消。

協(xié)程的團(tuán)隊(duì)在引入了結(jié)構(gòu)化并發(fā)(Structured concurrency)之后,他們就改變了協(xié)程構(gòu)建器功能launch()async()頂級(jí)更改為使用CoroutineScope接收器的擴(kuò)展。

coroutineScope方法

為了更加理解coroutineScope,看下下面示例:

  @Test
    fun main() {
        runBlocking {
            try {
                coroutineScope {
                    launch { // “1”
                        println("a")
                    }
                    launch {// “2”
                        println("b")
                        launch {// “3”
                            delay(1000)
                            println("c")
                            throw ArithmeticException("Hey!!")
                        }
                    }
                    val job = launch {// “4”
                        println("d")
                        delay(2000)
                        println("e")
                    }
                    job.join()
                    println("f")
                }
 
            } catch (e: Exception) {
                println("g")
            }
            println("h")
        }
    }

輸出結(jié)果:

a
b
d
c
g
h

會(huì)發(fā)現(xiàn)e,f沒有輸出。

原因:coroutineScope 是繼承外部 Job 的上下文創(chuàng)建作用域,在其內(nèi)部的取消操作是雙向傳播的,子協(xié)程未捕獲的異常也會(huì)向上傳遞給父協(xié)程。它更適合一系列對(duì)等的協(xié)程并發(fā)的完成一項(xiàng)工作,任何一個(gè)子協(xié)程異常退出,那么整體都將退出,簡單來說就是”一損俱損“。這也是協(xié)程內(nèi)部再啟動(dòng)子協(xié)程的默認(rèn)作用域。

coroutineSocpe啟動(dòng)了3個(gè)協(xié)程,“2”協(xié)程又啟動(dòng)了子協(xié)程“3”,子協(xié)程“3”因?yàn)閽伋霎惓H∠恕R驗(yàn)?code>coroutineSocpe異常時(shí)雙向的所以“3”會(huì)通知其父協(xié)程“2”取消,2會(huì)根據(jù)其作用域通知coroutineSocpe取消,這是一個(gè)自下而上的過程,coroutineSocpe取消會(huì)通知“4”取消,這是一個(gè)自上而下的過程。

其中join()delay()是支持取消的,所以這兩處就被取消了e,f就沒有被打出來了。

這里有一個(gè)小細(xì)節(jié)我們可以對(duì)coroutineSocpe內(nèi)部協(xié)程中的異常直接try...catch...捕獲掉表明協(xié)程把異步的異常處理到同步代碼邏輯當(dāng)中。

supervisorScope

再說一個(gè)和coroutineSocpe類似的supervisorScope

  @Test
    fun main() {
        runBlocking {
            try {
                supervisorScope{
                    launch { // “1”
                        println("a")
                    }
                    launch {// “2”
                        println("b")
                        launch {// “3”
                            delay(1000)
                            println("c")
                            throw ArithmeticException("Hey!!")
                        }
                    }
                    val job = launch {// “4”
                        println("d")
                        delay(2000)
                        println("e")
                    }
                    job.join()
                    println("f")
                }
 
            } catch (e: Exception) {
                println("g")
            }
            println("h")
        }
    }

輸出:

a
b
d
c
Exception in thread "main @coroutine#5" java.lang.ArithmeticException: Hey!!
    ...
e
f
h

會(huì)發(fā)現(xiàn)g沒有輸出。

原因:supervisorScope 同樣繼承外部作用域的上下文,但其內(nèi)部的取消操作是單向傳播的,父協(xié)程向子協(xié)程傳播,反過來則不然,這意味著子協(xié)程出了異常并不會(huì)影響父協(xié)程以及其他兄弟協(xié)程。它更適合一些獨(dú)立不相干的任務(wù),任何一個(gè)任務(wù)出問題,并不會(huì)影響其他任務(wù)的工作,簡單來說就是”自作自受“,例如 UI,我點(diǎn)擊一個(gè)按鈕出了異常,其實(shí)并不會(huì)影響手機(jī)狀態(tài)欄的刷新。需要注意的是,supervisorScope 內(nèi)部啟動(dòng)的子協(xié)程內(nèi)部再啟動(dòng)子協(xié)程,如無明確指出,則遵守默認(rèn)作用域規(guī)則,也即 supervisorScope 只作用域其直接子協(xié)程。

supervisorScope啟動(dòng)了3個(gè)協(xié)程,“2”協(xié)程又啟動(dòng)了子協(xié)程“3”,子協(xié)程“3”因?yàn)閽伋霎惓H∠?。但是因?yàn)?code>supervisorScope的取消操作是單向的即父協(xié)程向子協(xié)程傳播的,所以“3”協(xié)程并不會(huì)影響“2”協(xié)程

  @Test
    fun main() {
        val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
            println("${coroutineContext[CoroutineName]} $throwable")
        }
        runBlocking {
            try {
                supervisorScope {
                    launch {
                        // "1"
                        println("a")
                    }
                    launch(exceptionHandler + CoroutineName("\"2\"")) {
                        // "2"
                        println("b")
                        launch(exceptionHandler + CoroutineName("\"3\"")) {
                            //"3"
                            launch (exceptionHandler + CoroutineName("\"5\"")){// "5"
                                delay(1000)
                                println("c-")
                            }
                            println("c")
                            throw ArithmeticException("Hey!!")
                        }
                    }
                    val job = launch {
                        //"4"
                        println("d")
                        delay(2000)
                        println("e")
                    }
                    job.join()
                    println("f")
                }
 
            } catch (e: Exception) {
                println("g")
            }
            println("h")
        }
    }

仔細(xì)看下輸出:

a
b
d
c
CoroutineName("2") java.lang.ArithmeticException: Hey!!
e
f
h

異常竟然是協(xié)程“2”打出來的而且c-和g沒有打出來。

其實(shí)并不意外,supervisorScope 內(nèi)部啟動(dòng)的子協(xié)程內(nèi)部再啟動(dòng)子協(xié)程,如無明確指出,則遵守默認(rèn)作用域規(guī)則,也即 supervisorScope 只作用于其直接子協(xié)程。默認(rèn)作用域規(guī)則就是coroutineScope,子協(xié)程未捕獲的異常也會(huì)向上傳遞給父協(xié)程。

GlobeScope

看一個(gè)示例:

  fun work(i: Int) {
        Thread.sleep(1000)
        println("Work $i done")
    }
 
    @Test
    fun main() {
        val time = measureTimeMillis {
            runBlocking {
                for (i in 1..2) {
                    launch {
                        work(i)
                    }
                }
            }
        }
        println("Done in $time ms")
    }

輸出的結(jié)果:

Work 1 done
Work 2 done
Done in 2095 ms

它打印Work 1 done和Work 2 done,但它需要兩秒鐘才能完成。并發(fā)在哪里?launch已經(jīng)繼承了從引進(jìn)范圍協(xié)程調(diào)度runBlocking協(xié)同程序生成器,該組合限制住執(zhí)行到單個(gè)線程,所以這兩個(gè)任務(wù)在主線程中執(zhí)行順序。

要并發(fā)換成這樣就行了:

launch(Dispatchers.Default) {
    work(i)
}

這樣就能在1s中完成了。

如果我換成GlobalScope啟動(dòng)協(xié)同程序會(huì)發(fā)生什么?它應(yīng)該是相同的,因?yàn)樗诤笈_(tái)線程Dispatchers.Default中執(zhí)行協(xié)程。

    @Test
    fun main() {
        val time = measureTimeMillis {
            runBlocking {
                for (i in 1..2) {
                   GlobalScope.launch {
                        work(i)
                    }
                }
            }
        }
        println("Done in $time ms")
    }

輸出結(jié)果:

Done in 97 ms

并沒有打印Work x done,直接打印了Done in 97 ms。為什么?

原因:通過 GlobeScope 啟動(dòng)的協(xié)程單獨(dú)啟動(dòng)一個(gè)協(xié)程作用域,內(nèi)部的子協(xié)程遵從默認(rèn)的作用域規(guī)則。通過 GlobeScope 啟動(dòng)的協(xié)程“自成一派”。

GlobeScope.launch{...}launch(Dispatchers.Default){...}的區(qū)別就出來了。啟動(dòng)(Dispatchers.Default)runBlocking范圍內(nèi)創(chuàng)建子協(xié)程,因此runBlocking會(huì)自動(dòng)等待它們的完成。但是,GlobalScope.launch創(chuàng)建了全局協(xié)程。

我們可以通過以下手段控制來達(dá)到和launch(Dispatchers.Default){...}同樣的效果:

    @Test
    fun main() {
        val time = measureTimeMillis {
            runBlocking {
                val jobs = mutableListOf<Job>()
                for (i in 1..2) {
                    jobs += GlobalScope.launch {
                        work(i)
                    }
                }
                jobs.forEach { it.join() }
            }
        }
        println("Done in $time ms")
    }

現(xiàn)在輸出:

Work 1 done
Work 2 done
Done in 1102 ms

現(xiàn)在這個(gè)例子與GlobalScope代碼的工作方式類似launch(Dispatchers.Default),但需要付出更多努力,為什么還要編寫更多代碼?幾乎沒有理由GlobalScope在基于Kotlin協(xié)同程序的應(yīng)用程序中使用。

對(duì)于上面的操作還可以這樣:

  suspend fun work(i: Int) = withContext(Dispatchers.Default) {
        Thread.sleep(1000)
        println("Work $i done")
    }

tips:

  • 對(duì)于沒有協(xié)程作用域,但需要啟動(dòng)協(xié)程的時(shí)候,適合用 GlobalScope

  • 對(duì)于已經(jīng)有協(xié)程作用域的情況(例如通過 GlobalScope 啟動(dòng)的協(xié)程體內(nèi)),直接用協(xié)程啟動(dòng)器啟動(dòng)

  • 對(duì)于明確要求子協(xié)程之間相互獨(dú)立不干擾時(shí),使用 supervisorScope

  • 對(duì)于通過標(biāo)準(zhǔn)庫 API 創(chuàng)建的協(xié)程,這樣的協(xié)程比較底層,沒有 Job、作用域等概念的支撐,例如我們前面提到過 suspend main 就是這種情況,對(duì)于這種情況優(yōu)先考慮通過 coroutineScope 創(chuàng)建作用域;更進(jìn)一步,大家盡量不要直接使用標(biāo)準(zhǔn)庫 API,除非你對(duì) Kotlin 的協(xié)程機(jī)制非常熟悉。

launch

fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    // ...
): Job

它被定義為CoroutineScope上的擴(kuò)展函數(shù),并將CoroutineContext作為參數(shù),因此它實(shí)際上需要兩個(gè)協(xié)程上下文(因?yàn)榉秶皇菍?duì)上下文的引用)。
它與它們有什么關(guān)系?它使用plus運(yùn)算符合并它們,生成其元素的集合,以便context參數(shù)中的元素優(yōu)先于作用域中的元素。生成的上下文用于啟動(dòng)新的協(xié)程,但它不是新協(xié)程的上下文而是新協(xié)程的父上下文。新的協(xié)程創(chuàng)建自己的子Job實(shí)例(使用此上下文中的job作為其父)并將其子上下文定義為父上下文plus其job:

圖片來自于:Coroutine Context and Scope

a,按照慣例,CoroutineScope中的上下文包含一個(gè)Job,它將成為新的coroutine的父級(jí)(GlobalScope除外,你應(yīng)該避免)。

b,啟動(dòng)時(shí)的CoroutineContext參數(shù)是提供額外的上下文元素來覆蓋否則將從父作用域繼承的元素。

c,按照慣例,我們通常不會(huì)在上下文參數(shù)中傳遞Job來啟動(dòng),因?yàn)檫@會(huì)破壞父子關(guān)系,除非我們明確想要使用NonCancellable作業(yè)來打破它。

d,按照慣例,所有協(xié)程構(gòu)建器作用域的coroutineContext屬性與在此block內(nèi)運(yùn)行的協(xié)同程序的上下文相同。

 @Test
    fun main() = runBlocking<Unit> {
        launch { scopeCheck(this) }
    }
 
    suspend fun scopeCheck(scope: CoroutineScope) {
        println(scope.coroutineContext === coroutineContext)
    }

輸出為:true

e,由于上下文和范圍在本質(zhì)上是相同的,我們可以在沒有訪問范圍的情況下啟動(dòng)協(xié)程,而不使用GlobalScope只需將當(dāng)前coroutineContext包裝到CoroutineScope的實(shí)例中,如以下函數(shù)所示:

suspend fun doNotDoThis() {
    CoroutineScope(coroutineContext).launch {
        println("I'm confused")
    }
}

不要這樣做!它使協(xié)程的啟動(dòng)范圍變得不透明和隱含,捕獲一些外部Job來啟動(dòng)一個(gè)新的協(xié)程,而不在函數(shù)簽名中明確地宣布它。協(xié)程是與您的其余代碼同時(shí)進(jìn)行的一項(xiàng)工作,其啟動(dòng)必須是明確的.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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