前言
使用 kotlin 協(xié)程已經(jīng)幾年了雏胃,可以說(shuō)它極大地簡(jiǎn)化了多線程問(wèn)題的復(fù)雜度遗嗽,非常值得學(xué)習(xí)和掌握掂林。此文介紹并梳理協(xié)程的相關(guān)概念:suspend臣缀、non-blocking、Scope泻帮、Job精置、CoroutineContext、Dispatchers 和結(jié)構(gòu)化并發(fā)锣杂。
進(jìn)入?yún)f(xié)程世界
簡(jiǎn)而言之脂倦,協(xié)程是可以在其內(nèi)部進(jìn)行掛起操作的實(shí)例,是否支持掛起函數(shù)也是協(xié)程世界和非協(xié)程世界的最大區(qū)別元莫。初學(xué)者可以把協(xié)程看作是“輕量級(jí)線程”以做對(duì)比赖阻,但實(shí)際上他依然是跑在線程上的,所以也可以將它看作是一個(gè)強(qiáng)大的異步框架踱蠢。
要使用協(xié)程火欧,需要添加 kotlinx-coroutines-core
庫(kù)的依賴。
掛起函數(shù)與非阻塞
先看一段代碼:
import kotlinx.coroutines.*
fun main() = runBlocking { // this: CoroutineScope
println("Hello World!")
}
runBlocking 是一個(gè)協(xié)程構(gòu)造器茎截,他連接了非協(xié)程和協(xié)程世界苇侵,{ } 里便是協(xié)程世界。這兩個(gè)世界的差異在于是否可支持掛起操作:
import kotlinx.coroutines.*
fun main() = runBlocking { // this: CoroutineScope
delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
println("Hello World!")
}
"Hello World!" 將會(huì)在進(jìn)入?yún)f(xié)程 1s 后打印企锌,delay 便是一個(gè)掛起函數(shù)榆浓,類似線程中 sleep 的作用。區(qū)別是掛起函數(shù) delay 并不會(huì)阻塞當(dāng)前線程撕攒。這里有兩個(gè)概念陡鹃,掛起和非阻塞,掛起的是協(xié)程打却,非阻塞的是線程杉适。
從線程的角度看,假如進(jìn)入?yún)f(xié)程時(shí)運(yùn)行在線程 A柳击,那么執(zhí)行 delay 函數(shù)時(shí)運(yùn)行在線程 B猿推,執(zhí)行完 delay 之后又在線程 C 上執(zhí)行 println,線程 C 可能就是線程 A,也可能不是蹬叭,整個(gè)過(guò)程可以簡(jiǎn)化為:在某個(gè)線程執(zhí)行協(xié)程藕咏,在遇到掛起函數(shù) delay 時(shí)切換到另一個(gè)線程,執(zhí)行完 delay 后又切回某個(gè)線程繼續(xù)執(zhí)行協(xié)程秽五。不阻塞線程即不阻塞掛起前協(xié)程所在的線程孽查,即 A 線程,A 線程可以繼續(xù)執(zhí)行其他任務(wù)坦喘。那這有什么意義呢盲再?試想?yún)f(xié)程一開(kāi)始運(yùn)行在 Android 中的 ui 線程,在掛起函數(shù)里執(zhí)行耗時(shí)的網(wǎng)絡(luò)請(qǐng)求瓣铣,網(wǎng)絡(luò)請(qǐng)求結(jié)束自動(dòng)回到協(xié)程答朋,繼續(xù)在 ui 線程上的執(zhí)行。一方面耗時(shí)任務(wù)未阻塞 ui 線程棠笑,另一方面完全消除了異步回調(diào)梦碗,這使異步任務(wù)變得極為簡(jiǎn)單:以同步的方式書(shū)寫(xiě)異步的代碼。
從協(xié)程的角度看蓖救,協(xié)程遇到掛起函數(shù)時(shí)會(huì)被掛起洪规,即暫停了,等待掛起函數(shù)執(zhí)行完成循捺,相比于線程阻塞斩例,協(xié)程掛起幾乎沒(méi)有任何資源消耗,其本質(zhì)上是回調(diào)巨柒。
另外樱拴,掛起函數(shù)都有 suspend 關(guān)鍵字修飾,編譯器也會(huì)在此施加魔法:
public suspend fun delay(timeMillis: Long) { ... }
runBlocking 會(huì)阻塞當(dāng)前線程直到協(xié)程執(zhí)行完畢洋满,因此適合單元測(cè)試,下面會(huì)介紹更合適的進(jìn)入?yún)f(xié)程世界的方式珍坊。
注:例子中的線程 A牺勾、B、C 是通過(guò)協(xié)程中的 Dispatcher 控制的阵漏,后面聊到驻民。
CoroutineScope
協(xié)程都是由 CoroutineScope
創(chuàng)建的,協(xié)程在創(chuàng)建時(shí)履怯,都會(huì)關(guān)聯(lián)到一個(gè)新的的 CoroutineScope
回还。
CoroutineScope
即協(xié)程作用域,它限制和控制協(xié)程的作用范圍或者說(shuō)生命周期叹洲。不僅當(dāng)前協(xié)程會(huì)受其影響柠硕,所有在協(xié)程作用域內(nèi)創(chuàng)建的子協(xié)程也會(huì)有關(guān)聯(lián)。當(dāng)調(diào)用 CoroutineScope
的 cancel 方法時(shí),會(huì)取消當(dāng)前協(xié)程以及其關(guān)聯(lián)的所有下層協(xié)程蝗柔。
自定義 CoroutineScope
進(jìn)入?yún)f(xié)程世界除了上面使用的 runBlocking 方式闻葵。還可以自定義 CoroutineScope
:
fun main() {
CoroutineScope(Dispatchers.IO).launch {
...
}
}
上面代碼創(chuàng)建了一個(gè)運(yùn)行在 IO 線程環(huán)境的協(xié)程作用域并創(chuàng)建了一個(gè)協(xié)程,該協(xié)程運(yùn)行在 IO 線程癣丧。
GlobalScope
此外槽畔,還可以使用 GlobalScope
這個(gè)全局的協(xié)程作用域進(jìn)入?yún)f(xié)程世界:
fun main() {
GlobalScope.launch(Dispatchers.IO) {
...
}
}
GlobalScope
雖拿來(lái)即用,但它是全局的胁编,生命周期太長(zhǎng)厢钧,使用不當(dāng)會(huì)導(dǎo)致內(nèi)存泄露風(fēng)險(xiǎn)。
Android 中的 Scope
android 中嬉橙,推薦使用 LifecycleOwner.lifecycleScope
坏快,他和 LifecycleOwner 的生命周期綁定,不會(huì)出現(xiàn)內(nèi)存泄露的問(wèn)題憎夷。
如果使用了 ViewModel莽鸿,還可以使用 ViewModel.viewModelScope
,同樣和 ViewModel 生命周期綁定拾给。
它們都在 UI 線程執(zhí)行祥得。
還有一個(gè) MainScope
也在 UI 線程,有了上面兩個(gè)蒋得,這個(gè)基本用不到了级及,因?yàn)樗麤](méi)綁定有生命周期的對(duì)象,需要手動(dòng) cancel额衙。
Job
通過(guò) CoroutineScope
創(chuàng)建的協(xié)程即 Job
饮焦,可以認(rèn)為 就是協(xié)程的實(shí)例。一個(gè) Job 可以有多個(gè)子 窍侧,也即一個(gè)協(xié)程可以有多個(gè)子協(xié)程县踢。具有父子關(guān)系的協(xié)程,父協(xié)程取消時(shí)伟件,所有子協(xié)程都會(huì)取消硼啤, 的 cancel 本質(zhì)就是通過(guò)取消 實(shí)現(xiàn)的,因此 的 cancel 等價(jià)于 的 cancel斧账。
val job = launch { // 1
launch { // 2
...
}
launch { // 3
...
}
}
job.cancel()
上面代碼中 job 取消時(shí)會(huì)把 2谴返、3處協(xié)程也取消。
協(xié)程層次化的好處是便于管理咧织,再多的協(xié)程嗓袱,只要它們具有相同的父協(xié)程,就可以方便地控制其生命周期习绢。在層次化的協(xié)程結(jié)構(gòu)中渠抹,取消事件自上而下,異常事件自下而上,這背后是結(jié)構(gòu)化并發(fā)的思想逼肯。
特別的耸黑,SupervisorJob
是一種特殊的 Job,唯一的區(qū)別在于異常傳播到 SupervisorJob
層會(huì)停止向上傳播篮幢,將異常交由 SupervisorJob
處理大刊,借助這一特點(diǎn)我們可以把異常傳播控制在一定范圍內(nèi)。異常傳播與處理的詳細(xì)介紹之后會(huì)單獨(dú)寫(xiě)~~
CoroutineContext
CoroutineContext
三椿,協(xié)程上下文缺菌,是 CoroutineScope
的唯一成員,是用于存放協(xié)程執(zhí)行環(huán)境的地方搜锰,如調(diào)度器(Dispatcher
)伴郁、異常處理器(CoroutineExceptionHandler
)、Job
等蛋叼。CoroutineContext
的主要目的是提供一個(gè)統(tǒng)一的方式來(lái)管理協(xié)程的執(zhí)行環(huán)境和屬性焊傅。
CoroutineScope
在創(chuàng)建協(xié)程時(shí)會(huì)把 CoroutineContext
傳遞下去,新創(chuàng)建的協(xié)程會(huì)繼承父協(xié)程或Scope 的 CoroutineContext
狈涮。
CoroutineContext
數(shù)據(jù)的使用類似 Map狐胎,根據(jù) Key 取值,如果子協(xié)程創(chuàng)建時(shí)指定了 CoroutineContext
歌馍,則會(huì)合并握巢,相同 Key 的值會(huì)被覆蓋。
fun main() {
CoroutineScope(Dispatchers.Main).launch(CoroutineName("My Coroutine")) {
println("My Coroutine name: ${coroutineContext[CoroutineName]}")
}
}
上面代碼創(chuàng)建了一個(gè)在主線程的協(xié)程作用域松却,并創(chuàng)建了一個(gè)協(xié)程暴浦,該協(xié)程指定了協(xié)程元素-CoroutineName,這將和 CoroutineScope 中的 Dispatchers.Main 合并成新的 CoroutineContext
晓锻。
Dispatchers 與線程
Dispatchers
可以指定協(xié)程的執(zhí)行的線程環(huán)境歌焦,不過(guò)它強(qiáng)調(diào)的是線程的類別而不是哪一個(gè)具體的線程。如:Dispatchers.IO 表示 IO 密集型線程池带射,Dispatchers.Default 表示 cpu 密集型線程池同规,特別的是,Dispatchers.Main 特指 Android 中的主線程窟社。在協(xié)程中可以使用 withContext
進(jìn)行線程池的切換:
fun main() {
CoroutineScope(Dispatchers.Main).launch {
withContext(Dispatchers.IO) {
...
}
}
}
如果是網(wǎng)絡(luò)數(shù)據(jù)傳輸?shù)?io 任務(wù),一定要使用 Dispatchers.IO绪钥,其余計(jì)算類耗時(shí)任務(wù)使用 Dispatchers.Default灿里,這是因?yàn)?Dispatchers.Default 線程池的線程數(shù)較少(和 cpu 核心數(shù)有關(guān)),而 Dispatchers.IO 線程池的線程數(shù)更多且可動(dòng)態(tài)調(diào)整程腹。io 任務(wù)往往等待時(shí)間更長(zhǎng)匣吊,使用 Dispatchers.Default 的話很容易占滿所有線程資源。
協(xié)程世界
如果已經(jīng)在協(xié)程世界中了,那么創(chuàng)建新的協(xié)程的方式就比較多了色鸳,launch社痛、async、coroutineScope命雀、supervisorScope 等都可以方便地創(chuàng)建不同需求的協(xié)程蒜哀。
launch、async
兩者都是協(xié)程構(gòu)建器吏砂,最大的區(qū)別是 async 有返回值而 launch 沒(méi)有撵儿。有人說(shuō)并行就用 async,其實(shí)它們都能并行狐血,只不過(guò)業(yè)務(wù)場(chǎng)景里往往都需要拿到返回值淀歇。
完整測(cè)試用例:
fun main() = runBlocking {
CoroutineScope(Dispatchers.IO).launch(CoroutineName("My Coroutine")) {
println("My Coroutine name: ${coroutineContext[CoroutineName]}")
coroutineScope {
launch {
println("task 1")
}
launch {
println("task 2")
}
}
coroutineScope {
val task1 = async {
delay(1000)
1
}
val task2 = async {
delay(1000)
2
}
println(task1.await() + task2.await())
}
}.join()
}
coroutineScope、supervisorScope
coroutineScope
匈织、supervisorScope
也都協(xié)程構(gòu)建器浪默,只不過(guò)它們會(huì)等待協(xié)程執(zhí)行結(jié)束才結(jié)束,有點(diǎn)像 runBlocking缀匕,但不同的是 runBlocking 阻塞當(dāng)前線程纳决,而它們只是掛起協(xié)程,一個(gè)是普通方法弦追,另外兩個(gè)則是掛起函數(shù)岳链。
它們的差別類似 Job
與 SupervisorJob
的差別,supervisorScope
中頂級(jí)子協(xié)程的發(fā)生異常不會(huì)影響其他頂級(jí)子協(xié)程劲件。
supervisorScope {
launch {
throw Exception("error")
delay(1000)
println("task 1")
}
launch {
delay(2000)
println("task 2")
}
}
上面代碼 task 2 可以被正常打印出來(lái)掸哑,即使兄弟協(xié)程發(fā)生了異常。
結(jié)構(gòu)化并發(fā)
它是一種編程范式零远,旨在通過(guò)結(jié)構(gòu)化的方式使并發(fā)編程 更清晰明確苗分、更高質(zhì)量牵辣、更易維護(hù)摔癣。
其核心有幾點(diǎn):
- 通過(guò)把多線程任務(wù)進(jìn)行結(jié)構(gòu)化的包裝,使其具有明確的開(kāi)始和結(jié)束點(diǎn)纬向,并確保其孵化出的所有任務(wù)在退出前全部完成择浊。
- 這種包裝允許結(jié)構(gòu)中線程發(fā)生的異常能夠傳播至結(jié)構(gòu)頂端的作用域,并且能夠被該語(yǔ)言原生異常機(jī)制捕獲逾条。
kotlin 協(xié)程設(shè)計(jì)中的協(xié)程關(guān)系琢岩、執(zhí)行順序、異常傳播/處理 都符合結(jié)構(gòu)化并發(fā)师脂。結(jié)構(gòu)化并發(fā)明確了并發(fā)任務(wù)什么時(shí)候開(kāi)始担孔,什么時(shí)候結(jié)束江锨,異常如何傳播,通過(guò)控制頂層結(jié)構(gòu)具柄就可實(shí)現(xiàn)整個(gè)并發(fā)結(jié)構(gòu)的取消糕篇、異常處理啄育,使復(fù)雜的并發(fā)問(wèn)題簡(jiǎn)單、清晰拌消、可控挑豌。可以說(shuō)拼坎,結(jié)構(gòu)化并發(fā)大大降低了并發(fā)編程的難度浮毯。