CoroutineScope:
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
定義新協程的范圍。每個協程構建器都是CoroutineScope的擴展,并繼承其coroutineContext以自動傳播上下文元素和取消。
獲取范圍的獨立實例的最佳方法是CoroutineScope()和MainScope()工廠函數??梢允褂胮lus運算符將其他上下文元素附加到作用域。
建議不要手動實現此接口,應優先考慮通過委派實現。按照慣例,作用域的上下文應包含作業實例以強制執行結構化并發。
每個協同程序構建器(如launch,async等)和每個作用域函數(如coroutineScope,withContext等)都會將自己的作用域實例提供給它運行的內部代碼塊。按照慣例,它們都會等待塊內的所有協同程序在完成自己之前完成,從而強制執行結構化并發規則。
CoroutineScope應該在具有明確定義的生命周期的實體上實現(或用作字段),這些實體負責啟動子協同程序
CoroutineScope
是必須的么?其實不是的。當協程還是實驗性質的時候Kotlin 1.1時,我們啟動協程是可以這樣寫的:
fun requestSomeData() {
launch {
updateUI(performRequest())
}
}
這里我們在UI上下文中啟動一個新的協同程序launch(UI),調用掛起函數performRequest
對后端進行異步調用而不阻塞主UI線程,然后用結果更新UI。每個requestSomeData
調用創建自己的協程,它很好,不是嗎?
但這是一個問題。如果網絡或后端出現問題,這些異步操作可能需要很長時間才能完成。而且,這些操作通常在某些UI元素(如窗口或頁面)的范圍內執行。如果操作需要很長時間才能完成,則典型用戶會關閉相應的UI元素并執行其他操作,或者更糟糕的是,重新打開此UI并一次又一次地嘗試操作。但是我們之前的操作仍然在后臺運行,當用戶關閉相應的UI元素時,我們需要一些機制來取消它。
一個簡單的launch { … }
易于編寫,但它不是你應該寫的。
協同程序始終與應用程序中的某些本地作用域相關,這是一個生命周期有限的實體,如UI元素。因此,對于結構化并發,我們現在要求在CoroutineScope
中調用啟動,CoroutineScope
是由您的終身受限對象(如UI元素或其對應的視圖模型)實現的接口。
對于更新UI操作CoroutineScope
提供專門的實現,在這里可以看到
對于那些需要全局協程,其生命周期受應用程序生命周期限制的極少數情況,我們現在提供GlobalScope
對象,因此之前為全局協程啟動launch{...}
,現在變為GlobalScope.launch {...}
,這個協同程序的全局特性在代碼中變得明確。GlobalScope
在之前的幾章中經常用到的。
emmm............加入CoroutineScope
就只是解決了這個異步操作的問題么?
再看下面示例:
suspend fun loadAndCombine(name1: String, name2: String): Image {
val deferred1 = async { loadImage(name1) }
val deferred2 = async { loadImage(name2) }
return combineImages(deferred1.await(), deferred2.await())
}
這個例子看起來不錯,這個suspend函數
最終會在某個協程內部調用,異步下載2張圖片然后合并成一張,但是還是有很多微妙的錯誤,如果這個協程取消怎么辦?然后加載兩個圖片的異步任務仍然沒有受到影響,這不是一個可靠的代碼。
那在父協程取消的時候把子協程都取消不就可以了,改成這樣async(coroutineContext) { … }
。
它仍然還是有問題,比如下載第一張圖片失敗了,則deferred1.await()拋出了相應的異常,但是加載第二張圖片的協程仍然在后臺工作,解決這個問題就更加復雜了。
一個簡單async { … }
易于編寫,但它不是你應該寫的。
使用結構化并發async協同程序構建器CoroutineScope
就像是一樣成為擴展launch。你不能簡單地寫async { … }
,你必須提供范圍。一個適當的并行分解的例子變成:
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
協同程序都成為此范圍的子代,如果范圍因異常而失敗或被取消,則所有子代也將被取消。
協程的團隊在引入了結構化并發(Structured concurrency)之后,他們就改變了協程構建器功能launch()
和async()
從頂級更改為使用CoroutineScope接收器的擴展。
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")
}
}
輸出結果:
a
b
d
c
g
h
會發現e,f沒有輸出。
原因:coroutineScope
是繼承外部 Job 的上下文創建作用域,在其內部的取消操作是雙向傳播的,子協程未捕獲的異常也會向上傳遞給父協程。它更適合一系列對等的協程并發的完成一項工作,任何一個子協程異常退出,那么整體都將退出,簡單來說就是”一損俱損“。這也是協程內部再啟動子協程的默認作用域。
coroutineSocpe
啟動了3個協程,“2”協程又啟動了子協程“3”,子協程“3”因為拋出異常取消了。因為coroutineSocpe
異常時雙向的所以“3”會通知其父協程“2”取消,2會根據其作用域通知coroutineSocpe
取消,這是一個自下而上的過程,coroutineSocpe取消會通知“4”取消,這是一個自上而下的過程。
其中join()
和delay()
是支持取消的,所以這兩處就被取消了e,f就沒有被打出來了。
這里有一個小細節我們可以對coroutineSocpe
內部協程中的異常直接try...catch...捕獲掉表明協程把異步的異常處理到同步代碼邏輯當中。
supervisorScope
再說一個和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
會發現g沒有輸出。
原因:supervisorScope
同樣繼承外部作用域的上下文,但其內部的取消操作是單向傳播的,父協程向子協程傳播,反過來則不然,這意味著子協程出了異常并不會影響父協程以及其他兄弟協程。它更適合一些獨立不相干的任務,任何一個任務出問題,并不會影響其他任務的工作,簡單來說就是”自作自受“,例如 UI,我點擊一個按鈕出了異常,其實并不會影響手機狀態欄的刷新。需要注意的是,supervisorScope
內部啟動的子協程內部再啟動子協程,如無明確指出,則遵守默認作用域規則,也即 supervisorScope
只作用域其直接子協程。
supervisorScope
啟動了3個協程,“2”協程又啟動了子協程“3”,子協程“3”因為拋出異常取消了。但是因為supervisorScope
的取消操作是單向的即父協程向子協程傳播的,所以“3”協程并不會影響“2”協程
@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")
}
}
仔細看下輸出:
a
b
d
c
CoroutineName("2") java.lang.ArithmeticException: Hey!!
e
f
h
異常竟然是協程“2”打出來的而且c-和g沒有打出來。
其實并不意外,supervisorScope
內部啟動的子協程內部再啟動子協程,如無明確指出,則遵守默認作用域規則,也即 supervisorScope
只作用于其直接子協程。默認作用域規則就是coroutineScope
,子協程未捕獲的異常也會向上傳遞給父協程。
GlobeScope
看一個示例:
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")
}
輸出的結果:
Work 1 done
Work 2 done
Done in 2095 ms
它打印Work 1 done和Work 2 done,但它需要兩秒鐘才能完成。并發在哪里?launch已經繼承了從引進范圍協程調度runBlocking
協同程序生成器,該組合限制住執行到單個線程,所以這兩個任務在主線程中執行順序。
要并發換成這樣就行了:
launch(Dispatchers.Default) {
work(i)
}
這樣就能在1s中完成了。
如果我換成GlobalScope
啟動協同程序會發生什么?它應該是相同的,因為它在后臺線程Dispatchers.Default
中執行協程。
@Test
fun main() {
val time = measureTimeMillis {
runBlocking {
for (i in 1..2) {
GlobalScope.launch {
work(i)
}
}
}
}
println("Done in $time ms")
}
輸出結果:
Done in 97 ms
并沒有打印Work x done,直接打印了Done in 97 ms。為什么?
原因:通過 GlobeScope 啟動的協程單獨啟動一個協程作用域,內部的子協程遵從默認的作用域規則。通過 GlobeScope 啟動的協程“自成一派”。
GlobeScope.launch{...}
和launch(Dispatchers.Default){...}
的區別就出來了。啟動(Dispatchers.Default)
在runBlocking
范圍內創建子協程,因此runBlocking
會自動等待它們的完成。但是,GlobalScope.launch
創建了全局協程。
我們可以通過以下手段控制來達到和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")
}
現在輸出:
Work 1 done
Work 2 done
Done in 1102 ms
現在這個例子與GlobalScope
代碼的工作方式類似launch(Dispatchers.Default)
,但需要付出更多努力,為什么還要編寫更多代碼?幾乎沒有理由GlobalScope
在基于Kotlin協同程序的應用程序中使用。
對于上面的操作還可以這樣:
suspend fun work(i: Int) = withContext(Dispatchers.Default) {
Thread.sleep(1000)
println("Work $i done")
}
tips:
對于沒有協程作用域,但需要啟動協程的時候,適合用
GlobalScope
對于已經有協程作用域的情況(例如通過
GlobalScope
啟動的協程體內),直接用協程啟動器啟動對于明確要求子協程之間相互獨立不干擾時,使用
supervisorScope
對于通過標準庫 API 創建的協程,這樣的協程比較底層,沒有 Job、作用域等概念的支撐,例如我們前面提到過 suspend main 就是這種情況,對于這種情況優先考慮通過
coroutineScope
創建作用域;更進一步,大家盡量不要直接使用標準庫 API,除非你對 Kotlin 的協程機制非常熟悉。
launch
fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
// ...
): Job
它被定義為CoroutineScope
上的擴展函數,并將CoroutineContext
作為參數,因此它實際上需要兩個協程上下文(因為范圍只是對上下文的引用)。
它與它們有什么關系?它使用plus運算符合并它們,生成其元素的集合,以便context參數中的元素優先于作用域中的元素。生成的上下文用于啟動新的協程,但它不是新協程的上下文而是新協程的父上下文。新的協程創建自己的子Job實例(使用此上下文中的job作為其父)并將其子上下文定義為父上下文plus其job:
圖片來自于:Coroutine Context and Scope
a,按照慣例,CoroutineScope
中的上下文包含一個Job
,它將成為新的coroutine的父級
(GlobalScope除外,你應該避免)。
b,啟動時的CoroutineContext
參數是提供額外的上下文元素來覆蓋否則將從父作用域繼承的元素。
c,按照慣例,我們通常不會在上下文參數中傳遞Job來啟動,因為這會破壞父子關系,除非我們明確想要使用NonCancellable
作業來打破它。
d,按照慣例,所有協程構建器作用域的coroutineContext
屬性與在此block內運行的協同程序的上下文相同。
@Test
fun main() = runBlocking<Unit> {
launch { scopeCheck(this) }
}
suspend fun scopeCheck(scope: CoroutineScope) {
println(scope.coroutineContext === coroutineContext)
}
輸出為:true
e,由于上下文和范圍在本質上是相同的,我們可以在沒有訪問范圍的情況下啟動協程,而不使用GlobalScope
只需將當前coroutineContext
包裝到CoroutineScope
的實例中,如以下函數所示:
suspend fun doNotDoThis() {
CoroutineScope(coroutineContext).launch {
println("I'm confused")
}
}
不要這樣做!它使協程的啟動范圍變得不透明和隱含,捕獲一些外部Job來啟動一個新的協程,而不在函數簽名中明確地宣布它。協程是與您的其余代碼同時進行的一項工作,其啟動必須是明確的.