協(xié)程簡介
Kotlin 中的協(xié)程提供了一種全新處理并發(fā)的方式,可以在 Android 平臺上使用它來簡化異步執(zhí)行的代碼。協(xié)程是從 Kotlin 1.3 版本開始引入,但這一概念在編程世界誕生的黎明之際就有了,最早使用協(xié)程的編程語言可以追溯到 1967 年的 Simula 語言。
在過去幾年間,協(xié)程這個概念發(fā)展勢頭迅猛,現(xiàn)已經(jīng)被諸多主流編程語言采用,比如 Javascript、C#、Python、Ruby 以及 Go 等。Kotlin 的協(xié)程是基于來自其他語言的既定概念。
在 Android 平臺上,協(xié)程主要用來解決兩個問題:
- 處理耗時任務(wù) (Long running tasks),這種任務(wù)常常會阻塞住主線程;
- 保證主線程安全 (Main-safety) ,即確保安全地從主線程調(diào)用任何 suspend 函數(shù)。
從本質(zhì)上來說,協(xié)程就是一個輕量級的線程。
一、協(xié)程的基本使用
在使用協(xié)程之前,我們需要先引入Coroutine 的包
// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.32"
// 協(xié)程核心庫
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3"
// 協(xié)程Android支持庫
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3"
// 協(xié)程Java8支持庫
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.4.3"
創(chuàng)建協(xié)程的方式有很多種,這里不延伸協(xié)程的高級用法(熱數(shù)據(jù)通道Channel、冷數(shù)據(jù)流Flow.…),創(chuàng)建協(xié)程這里介紹常用的三種方式:
1.1、runBlocking 啟動
runBlocking {
println("runBlocking 啟動一個協(xié)程")
}
runBlocking 啟動一個協(xié)程會阻塞調(diào)用它的線程,只到里面的代碼執(zhí)行結(jié)束,返回值是泛型T。
1.2、GlobalScope.launch 啟動
GlobalScope.launch {
println("launch 啟動一個協(xié)程")
}
launch啟動一個協(xié)程不會阻塞調(diào)用線程,必須要在協(xié)程作用域(CoroutineScope)中才能調(diào)用,返回值是一個Job
1.3、GlobalScope.async 啟動
GlobalScope.async {
println("async 啟動一個協(xié)程")
}
async啟動一個協(xié)程其實和launch 是一樣的,不同點在于async的返回參數(shù)是: Deferred Deferred<out T> : Job
,它實現(xiàn)了一個Deferred接口,但是Deferred 繼承了job,Deferred和job 的不同點是,Deferred 里面定義了await 函數(shù),需要與await()
掛起函數(shù)結(jié)合使用。
1.4、三種啟動方式的說明
-
runBlocking{} - 主要用于測試
該方法的設(shè)計目的是讓suspend風(fēng)格編寫的庫能夠在常規(guī)阻塞代碼中使用,常在main方法和測試中使用。
-
GlobalScope.launch/async{} - 不推薦使用
由于這樣啟動的協(xié)程存在啟動協(xié)程的組件已被銷毀但協(xié)程還存在的情況,極限情況下可能導(dǎo)致資源耗盡,因此并不推薦這樣啟動,尤其是在客戶端這種需要頻繁創(chuàng)建銷毀組件的場景。
二、Coroutine 源碼解析
這里我們使用CoroutineScope.launch{}
的源碼為例,來深入了解Coroutine:
上面是launch
函數(shù)的定義,它以CoroutineScope
的擴展函數(shù)的形成出現(xiàn),函數(shù)參數(shù)分別是:協(xié)程上下文CoroutineContext
、協(xié)程啟動模式CoroutineStart
、協(xié)程體
,返回值是協(xié)程實例Job
,其中CoroutineContext
又包括了Job
、CoroutineDispatcher
、CoroutineName
。下面我們就一一介紹這些內(nèi)容:CoroutineContext
、Job
、CoroutineDispatcher
、CoroutineStart
、CoroutineScope
。
2.1、CoroutineContext
CoroutineContext: 協(xié)程上下文
- 線程行為、生命周期、異常以及調(diào)試
- 包含用戶定義的一些數(shù)據(jù)集合,這些數(shù)據(jù)與協(xié)程密切相關(guān)
- 它是一個有索引的 Element 實例集合,一個介于 set 和 map之間的數(shù)據(jù)結(jié)構(gòu)。每個 element 在這個集合有一個唯一的 Key
-
Job: 控制協(xié)程的生命周期
-
CoroutineDispatcher: 向合適的線程分發(fā)任務(wù)
-
CoroutineName: 協(xié)程的名稱,調(diào)試的時候很有用
-
CoroutineExceptionHandler: 處理未被捕捉的異常
CoroutineContext
有兩個非常重要的元素 — Job
和 Dispatcher
,Job
是當(dāng)前的 Coroutine
實例而 Dispatcher
決定了當(dāng)前 Coroutine
執(zhí)行的線程,還可以添加CoroutineName
,用于調(diào)試,添加 CoroutineExceptionHandler
用于捕獲異常,它們都實現(xiàn)了Element
接口。
fun main() {
val coroutineContext = Job() + Dispatchers.Default + CoroutineName("myContext")
println("$coroutineContext,${coroutineContext[CoroutineName]}")
val newCoroutineContext = coroutineContext.minusKey(CoroutineName)
println("$newCoroutineContext")
}
輸出結(jié)果:
CoroutineContext 源碼:
通過源碼我可以看到CoroutineContext
定義了四個核心的操作:
-
操作符get
可以通過key
來獲取這個Element
。由于這是一個get
操作符,所以可以像訪問 map 中的元素一樣使用context[key]
這種中括號的形式來訪問。 -
操作符 plus
和Set.plus
擴展函數(shù)類似,返回一個新的context
對象,新的對象里面包含了兩個里面的所有Element
,如果遇到重復(fù)的(Key 一樣的),那么用+
號右邊的Element
替代左邊的。+
運算符可以很容易的用于結(jié)合上下文,但是有一個很重要的事情需要小心 —— 要注意它們結(jié)合的次序,因為這個+
運算符是不對稱的。 -
fun fold(initial: R, operation: (R, Element) -> R): R
和Collection.fold
擴展函數(shù)類似,提供遍歷當(dāng)前context
中所有Element
的能力。 -
fun minusKey(key: Key<>): CoroutineContext
返回一個上下文,其中包含該上下文中的元素,但不包含具有指定key
的元素。
2.2、Job 源碼
Job 用于處理協(xié)程
對于每一個所創(chuàng)建的協(xié)程 (通過 launch
或者 async),它會返回一個 Job
實例,該實例是協(xié)程的唯一標識,并且負責(zé)管理協(xié)程的生命周期CoroutineScope.launch
函數(shù)返回的是一個 Job
對象,代表一個異步的任務(wù)。Job
具有生命周期并且可以取消。 Job
還可以有層級關(guān)系,一個Job
可以包含多個子Job
,當(dāng)父Job
被取消后,所有的子Job
也會被自動取消;當(dāng)子Job
被取消或者出現(xiàn)異常后父Job
也會被取消。
除了通過 CoroutineScope.launch
來創(chuàng)建Job
對象之外,還可以通過 Job()
工廠方法來創(chuàng)建該對象。默認情況下,子Job
的失敗將會導(dǎo)致父Job
被取消,這種默認的行為可以通過 SupervisorJob
來修改。
具有多個子 Job
的父Job
會等待所有子Job
完成(或者取消)后,自己才會執(zhí)行完成
Job 生命周期的狀態(tài)
一個任務(wù)可以包含一系列狀態(tài): 新創(chuàng)建 (New)、活躍 (Active)、完成中 (Completing)、已完成 (Completed)、取消中 (Cancelling) 和已取消 (Cancelled)。雖然我們無法直接訪問這些狀態(tài),但是我們可以訪問 Job
的屬性: isActive
、isCancelled
和 isCompleted
。
如果協(xié)程處于活躍狀態(tài),協(xié)程運行出錯或者調(diào)用 job.cancel()
都會將當(dāng)前任務(wù)置為取消中 (Cancelling) 狀態(tài) (isActive = false, isCancelled = true
)。當(dāng)所有的子協(xié)程都完成后,協(xié)程會進入已取消 (Cancelled) 狀態(tài),此時 isCompleted = true
。
2.3、Job 的常用函數(shù)
這些函數(shù)都是線程安全的,所以可以直接在其他
Coroutine
中調(diào)用。
-
fun start(): Boolean
調(diào)用該函數(shù)來啟動這個Coroutine
,如果當(dāng)前Coroutine
還沒有執(zhí)行調(diào)用該函數(shù)返回true
,如果當(dāng)前Coroutine
已經(jīng)執(zhí)行或者已經(jīng)執(zhí)行完畢,則調(diào)用該函數(shù)返回false
-
fun cancel(cause: CancellationException? = null)
通過可選的取消原因取消此作業(yè)。 原因可以用于指定錯誤消息或提供有關(guān)取消原因的其他詳細信息,以進行調(diào)試。 -
fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
通過這個函數(shù)可以給Job
設(shè)置一個完成通知,當(dāng)Job
執(zhí)行完成的時候會同步執(zhí)行這個通知函數(shù)。 回調(diào)的通知對象類型為:typealias CompletionHandler = (cause: Throwable?) -> Unit
.CompletionHandler
參數(shù)代表了Job
是如何執(zhí)行完成的。cause
有下面三種情況: -
- 如果
Job
是正常執(zhí)行完成的,則cause
參數(shù)為null
- 如果
-
- 如果
Job
是正常取消的,則cause
參數(shù)為CancellationException
對象。這種情況不應(yīng)該當(dāng)做錯誤處理,這是任務(wù)正常取消的情形。所以一般不需要在錯誤日志中記錄這種情況。 - 其他情況表示
Job
執(zhí)行失敗了。
- 如果
這個函數(shù)的返回值為 DisposableHandle
對象,如果不再需要監(jiān)控 Job
的完成情況了, 則可以調(diào)用 DisposableHandle.dispose
函數(shù)來取消監(jiān)聽。如果 Job
已經(jīng)執(zhí)行完了, 則無需調(diào)用 dispose
函數(shù)了,會自動取消監(jiān)聽。
- suspend fun join()
join
函數(shù)和前面三個函數(shù)不同,這是一個 suspend
函數(shù)。所以只能在 Coroutine 內(nèi)調(diào)用。
這個函數(shù)會暫停當(dāng)前所處的 Coroutine
直到該Coroutine
執(zhí)行完成。所以 join
函數(shù)一般用來在另外一個 Coroutine
中等待 job
執(zhí)行完成后繼續(xù)執(zhí)行。當(dāng) Job
執(zhí)行完成后, job.join
函數(shù)恢復(fù),這個時候 job
這個任務(wù)已經(jīng)處于完成狀態(tài)了,而調(diào)用 job.join
的 Coroutine
還繼續(xù)處于 activie
狀態(tài)。
請注意,只有在其所有子級都完成后,作業(yè)才能完成
該函數(shù)的掛起是可以被取消的,并且始終檢查調(diào)用的Coroutine
的Job
是否取消。如果在調(diào)用此掛起函數(shù)或?qū)⑵鋻炱饡r,調(diào)用Coroutine
的Job
被取消或完成,則此函數(shù)將引發(fā) CancellationException
。
2.4、SupervisorJob
SupervisorJob
是一個頂層函數(shù),定義如下:
該函數(shù)創(chuàng)建了一個處于 active
狀態(tài)的supervisor job
。如前所述, Job
是有父子關(guān)系的,如果子Job
失敗了父Job
會自動失敗,這種默認的行為可能不是我們期望的。比如在 Activity
中有兩個子Job
分別獲取一篇文章的評論內(nèi)容和作者信息。如果其中一個失敗了,我們并不希望父Job
自動取消,這樣會導(dǎo)致另外一個子Job也被取消。而SupervisorJob
就是這么一個特殊的 Job
,里面的子Job
不相互影響,一個子Job
失敗了,不影響其他子Job
的執(zhí)行。SupervisorJob(parent:Job?)
具有一個parent
參數(shù),如果指定了這個參數(shù),則所返回的 Job
就是參數(shù) parent
的子Job
。如果 Parent Job
失敗了或者取消了,則這個 Supervisor Job
也會被取消。當(dāng) Supervisor Job
被取消后,所有 Supervisor Job
的子Job
也會被取消。
MainScope()
的實現(xiàn)就使用了 SupervisorJob
和一個 Main Dispatcher
:
但是SupervisorJob
是很容易被誤解的,它和協(xié)程異常處理、子協(xié)程所屬Job
類型還有域有很多讓人混淆的地方,具體異常處理可以看Google的這一篇文章:協(xié)程中的取消和異常 | 異常處理詳解
三、suspend關(guān)鍵字
這個 suspend
關(guān)鍵字,既然它并不是真正實現(xiàn)掛起,那它的作用是什么?
它其實是一個提醒。
函數(shù)的創(chuàng)建者對函數(shù)的使用者的提醒:我是一個耗時函數(shù),我被我的創(chuàng)建者用掛起的方式放在后臺運行,所以請在協(xié)程里調(diào)用我。
掛起的操作 —— 也就是切線程,依賴的是掛起函數(shù)里面的實際代碼,而不是這個關(guān)鍵字。
所以這個關(guān)鍵字,只是一個提醒。
3.1、CoroutineDispatcher 調(diào)度器
-
Dispatchers.Default
默認的調(diào)度器,適合處理后臺計算,是一個CPU密集型任務(wù)調(diào)度器。如果創(chuàng)建 Coroutine 的時候沒有指定 dispatcher,則一般默認使用這個作為默認值。Default dispatcher 使用一個共享的后臺線程池來運行里面的任務(wù)。注意它和IO共享線程池,只不過限制了最大并發(fā)數(shù)不同。
-
Dispatchers.IO
顧名思義這是用來執(zhí)行阻塞 IO 操作的,是和Default共用一個共享的線程池來執(zhí)行里面的任務(wù)。根據(jù)同時運行的任務(wù)數(shù)量,在需要的時候會創(chuàng)建額外的線程,當(dāng)任務(wù)執(zhí)行完畢后會釋放不需要的線程。
-
Dispatchers.Unconfined
由于Dispatchers.Unconfined未定義線程池,所以執(zhí)行的時候默認在啟動線程。遇到第一個掛起點,之后由調(diào)用resume的線程決定恢復(fù)協(xié)程的線程。
-
Dispatchers.Main
指定執(zhí)行的線程是主線程,在Android上就是UI線程。
由于子Coroutine 會繼承父Coroutine 的 context,所以為了方便使用,我們一般會在 父Coroutine 上設(shè)定一個 Dispatcher,然后所有 子Coroutine 自動使用這個 Dispatcher。
3.2、CoroutineStart 協(xié)程啟動模式
-
CoroutineStart.DEFAULT
協(xié)程創(chuàng)建后立即開始調(diào)度,在調(diào)度前如果協(xié)程被取消,其將直接進入取消響應(yīng)的狀態(tài)雖然是立即調(diào)度,但也有可能在執(zhí)行前被取消
-
CoroutineStart.ATOMIC
協(xié)程創(chuàng)建后立即開始調(diào)度,協(xié)程執(zhí)行到第一個掛起點之前不響應(yīng)取消
雖然是立即調(diào)度,但其將調(diào)度和執(zhí)行兩個步驟合二為一了,就像它的名字一樣,其保證調(diào)度和執(zhí)行是原子操作,因此協(xié)程也一定會執(zhí)行 -
CoroutineStart.LAZY
只要協(xié)程被需要時,包括主動調(diào)用該協(xié)程的start、join或者await等函數(shù)時才會開始調(diào)度,如果調(diào)度前就被取消,協(xié)程將直接進入異常結(jié)束狀態(tài)
-
CoroutineStart.UNDISPATCHED
協(xié)程創(chuàng)建后立即在當(dāng)前函數(shù)調(diào)用棧中執(zhí)行,直到遇到第一個真正掛起的點
是立即執(zhí)行,因此協(xié)程一定會執(zhí)行
這些啟動模式的設(shè)計主要是為了應(yīng)對某些特殊的場景。業(yè)務(wù)開發(fā)實踐中通常使用DEFAULT和LAZY這兩個啟動模式就夠了
3.3、CoroutineScope - 協(xié)程作用域
定義協(xié)程必須指定其 CoroutineScope
。CoroutineScope
可以對協(xié)程進行追蹤,即使協(xié)程被掛起也是如此。同調(diào)度程序 (Dispatcher
) 不同,CoroutineScope
并不運行協(xié)程,它只是確保您不會失去對協(xié)程的追蹤。為了確保所有的協(xié)程都會被追蹤,Kotlin
不允許在沒有使用 CoroutineScope
的情況下啟動新的協(xié)程。CoroutineScope
可被看作是一個具有超能力的 ExecutorService
的輕量級版本。CoroutineScope
會跟蹤所有協(xié)程,同樣它還可以取消由它所啟動的所有協(xié)程。這在 Android
開發(fā)中非常有用,比如它能夠在用戶離開界面時停止執(zhí)行協(xié)程。
Coroutine
是輕量級的線程,并不意味著就不消耗系統(tǒng)資源。 當(dāng)異步操作比較耗時的時候,或者當(dāng)異步操作出現(xiàn)錯誤的時候,需要把這個 Coroutine
取消掉來釋放系統(tǒng)資源。在 Android
環(huán)境中,通常每個界面(Activity
、Fragment
等)啟動的 Coroutine
只在該界面有意義,如果用戶在等待 Coroutine
執(zhí)行的時候退出了這個界面,則再繼續(xù)執(zhí)行這個 Coroutine
可能是沒必要的。另外 Coroutine
也需要在適當(dāng)?shù)?context
中執(zhí)行,否則會出現(xiàn)錯誤,比如在非 UI
線程去訪問 View
。 所以 Coroutine
在設(shè)計的時候,要求在一個范圍(Scope
)內(nèi)執(zhí)行,這樣當(dāng)這個 Scope
取消的時候,里面所有的子 Coroutine
也自動取消。所以要使用 Coroutine
必須要先創(chuàng)建一個對應(yīng)的 CoroutineScope
。
CoroutineScope 接口
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
CoroutineScope
只是定義了一個新 Coroutine
的執(zhí)行 Scope
。每個 coroutine builder
都是 CoroutineScope
的擴展函數(shù),并且自動的繼承了當(dāng)前 Scope
的 coroutineContext
。
3.4、分類及行為規(guī)則
官方框架在實現(xiàn)復(fù)合協(xié)程的過程中也提供了作用域,主要用以明確寫成之間的父子關(guān)系,以及對于取消或者異常處理等方面的傳播行為。該作用域包括以下三種:
-
頂級作用域
沒有父協(xié)程的協(xié)程所在的作用域為頂級作用域。 -
協(xié)同作用域
協(xié)程中啟動新的協(xié)程,新協(xié)程為所在協(xié)程的子協(xié)程,這種情況下,子協(xié)程所在的作用域默認為協(xié)同作用域。此時子協(xié)程拋出的未捕獲異常,都將傳遞給父協(xié)程處理,父協(xié)程同時也會被取消。coroutineScope 內(nèi)部的異常會向上傳播,子協(xié)程未捕獲的異常會向上傳遞給父協(xié)程,任何一個子協(xié)程異常退出,會導(dǎo)致整體的退出
-
主從作用域
與協(xié)同作用域在協(xié)程的父子關(guān)系上一致,區(qū)別在于,處于該作用域下的協(xié)程出現(xiàn)未捕獲的異常時,不會將異常向上傳遞給父協(xié)程。supervisorScope屬于主從作用域,會繼承父協(xié)程的上下文,它的特點就是子協(xié)程的異常不會影響父協(xié)程
除了三種作用域中提到的行為以外,父子協(xié)程之間還存在以下規(guī)則:
- 父協(xié)程被取消,則所有子協(xié)程均被取消。由于協(xié)同作用域和主從作用域中都存在父子協(xié)程關(guān)系,因此此條規(guī)則都適用。
- 父協(xié)程需要等待子協(xié)程執(zhí)行完畢之后才會最終進入完成狀態(tài),不管父協(xié)程自身的協(xié)程體是否已經(jīng)執(zhí)行完。
- 子協(xié)程會繼承父協(xié)程的協(xié)程上下文中的元素,如果自身有相同
key
的成員,則覆蓋對應(yīng)的key
,覆蓋的效果僅限自身范圍內(nèi)有效。
四、Android中協(xié)程的使用及取消和異常
普通協(xié)程如果產(chǎn)生未處理異常會將此異常傳播至它的父協(xié)程,然后父協(xié)程會取消所有的子協(xié)程、取消自己、將異常繼續(xù)向上傳遞。下面拿一個官方的圖來示例這個過程:
這種情況有的時候并不是我們想要的,我們更希望一個協(xié)程在產(chǎn)生異常時,不影響其他協(xié)程的執(zhí)行,在上文中我們也提到了一些解決方案,下面我們就在實踐一下。
4.1、使用SupervisorJob
// 使用官方庫的 MainScope()獲取一個協(xié)程作用域用于創(chuàng)建協(xié)程
private val mScope = MainScope();
fun onClickCoroutine(view: View) {
mScope.launch(Dispatchers.Default) {
println("我是第一個協(xié)程")
}
mScope.launch(Dispatchers.Default) {
println("我是第二個協(xié)程")
throw RuntimeException("RuntimeException 就是一個異常")
}
mScope.launch(Dispatchers.Default) {
println("我是第三個協(xié)程")
}
}
代碼執(zhí)行結(jié)果:
MainScope()
之前提到過了,它的實現(xiàn)就是用了SupervisorJob
。執(zhí)行結(jié)果就是 第二個協(xié)程 拋出異常后,第三個協(xié)程 正常執(zhí)行了,但是程序崩了,因為我們沒有處理這個異常,下面完善一下代碼
異常處理:
fun onClickCoroutine(view: View) {
mScope.launch(Dispatchers.Default) {
println("我是第一個協(xié)程")
}
mScope.launch(Dispatchers.Default + CoroutineExceptionHandler { coroutineContext, throwable ->
println(
"CoroutineExceptionHandler: $throwable"
)
}) {
println("我是第二個協(xié)程")
throw RuntimeException("RuntimeException 就是一個異常")
}
mScope.launch(Dispatchers.Default) {
println("我是第三個協(xié)程")
}
}
打印結(jié)果:
程序沒有崩潰,并且異常處理的打印也輸出了,這就達到了我們想要的效果。但是要注意一個事情,這幾個子協(xié)程的父級是SupervisorJob
,但是他們再有子協(xié)程的話,他們的子協(xié)程的父級就不是SupervisorJob了,所以當(dāng)它們產(chǎn)生異常時,就不是我們演示的效果了。我們使用一個官方的圖來解釋這個關(guān)系:
如圖所示,新的協(xié)程被創(chuàng)建時,會生成新的 Job
實例替代 SupervisorJob
。
4.2、使用supervisorScope
這個作用域上文中也有提到,使用supervisorScope
也可以達到我們想要的效果,上代碼:
fun onClickCoroutine(view: View) {
val coroutineScope = CoroutineScope(Job() + Dispatchers.Default)
coroutineScope.launch(CoroutineExceptionHandler { coroutineContext, throwable ->
println(
"CoroutineExceptionHandler: $throwable"
)
}) {
supervisorScope {
launch {
println("我是第一個協(xié)程")
}
launch {
println("我是第二個協(xié)程")
throw RuntimeException("RuntimeException 就是一個異常")
}
launch {
println("我是第三個協(xié)程")
}
}
}
}
運行結(jié)果
可以看到已經(jīng)達到了我們想要的效果,但是如果將supervisorScope
換成coroutineScope
,結(jié)果就不是這樣了。最終還是拿官方的圖來展示:
文章來源:http://www.zghlxwxcb.cn/news/detail-466602.html
文章到這里就結(jié)束了,本文參考Quyunshuo,如有侵權(quán),請聯(lián)系刪除。文章來源地址http://www.zghlxwxcb.cn/news/detail-466602.html
到了這里,關(guān)于【kotlin 協(xié)程】萬字協(xié)程 一篇完成kotlin 協(xié)程進階的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!