kotlin協(xié)程:一文搞懂各種概念

前言

使用 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ù)岳链。

它們的差別類似 JobSupervisorJob 的差別,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ā)編程的難度浮毯。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市泰鸡,隨后出現(xiàn)的幾起案子债蓝,更是在濱河造成了極大的恐慌,老刑警劉巖盛龄,帶你破解...
    沈念sama閱讀 212,718評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件饰迹,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡余舶,警方通過(guò)查閱死者的電腦和手機(jī)啊鸭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)匿值,“玉大人赠制,你說(shuō)我怎么就攤上這事⌒荆” “怎么了钟些?”我有些...
    開(kāi)封第一講書(shū)人閱讀 158,207評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)绊谭。 經(jīng)常有香客問(wèn)我政恍,道長(zhǎng),這世上最難降的妖魔是什么达传? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,755評(píng)論 1 284
  • 正文 為了忘掉前任篙耗,我火速辦了婚禮,結(jié)果婚禮上宪赶,老公的妹妹穿的比我還像新娘宗弯。我一直安慰自己,他們只是感情好搂妻,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,862評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布罕伯。 她就那樣靜靜地躺著,像睡著了一般叽讳。 火紅的嫁衣襯著肌膚如雪追他。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 50,050評(píng)論 1 291
  • 那天岛蚤,我揣著相機(jī)與錄音邑狸,去河邊找鬼。 笑死涤妒,一個(gè)胖子當(dāng)著我的面吹牛单雾,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播她紫,決...
    沈念sama閱讀 39,136評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼硅堆,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了贿讹?” 一聲冷哼從身側(cè)響起渐逃,我...
    開(kāi)封第一講書(shū)人閱讀 37,882評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎民褂,沒(méi)想到半個(gè)月后茄菊,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,330評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡赊堪,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,651評(píng)論 2 327
  • 正文 我和宋清朗相戀三年面殖,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片哭廉。...
    茶點(diǎn)故事閱讀 38,789評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡脊僚,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出遵绰,到底是詐尸還是另有隱情辽幌,我是刑警寧澤,帶...
    沈念sama閱讀 34,477評(píng)論 4 333
  • 正文 年R本政府宣布街立,位于F島的核電站舶衬,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏赎离。R本人自食惡果不足惜逛犹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,135評(píng)論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望梁剔。 院中可真熱鬧虽画,春花似錦、人聲如沸荣病。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,864評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)个盆。三九已至脖岛,卻和暖如春朵栖,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背柴梆。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,099評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工陨溅, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人绍在。 一個(gè)月前我還...
    沈念sama閱讀 46,598評(píng)論 2 362
  • 正文 我出身青樓门扇,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親偿渡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子臼寄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,697評(píng)論 2 351

推薦閱讀更多精彩內(nèi)容