協(xié)程筆記

協(xié)程在Kotlin中文文檔的解釋是輕量級(jí)的線程纯路,Go姥份、Python 等很多現(xiàn)成語(yǔ)言在語(yǔ)言層面上都實(shí)現(xiàn)協(xié)程凡伊,不過(guò)Kotlin和他們不同的的是可帽,Kotlin協(xié)程本質(zhì)上只是一套基于原生Java線程池 的封裝钾军,Kotlin 協(xié)程的核心競(jìng)爭(zhēng)力在于:它能簡(jiǎn)化異步并發(fā)任務(wù),以同步方式寫(xiě)異步代碼鳄袍。

  • 掛起:suspend
    在協(xié)程里suspend是一個(gè)重要的關(guān)鍵字,這個(gè)關(guān)鍵字只是起到的提醒的作用吏恭,當(dāng)代碼執(zhí)行到suspend時(shí)拗小,會(huì)從當(dāng)前線程掛起這個(gè)函數(shù),然后代碼繼續(xù)執(zhí)行樱哼,而掛起的函數(shù)從當(dāng)前線程脫離哀九,然后繼續(xù)執(zhí)行,這個(gè)時(shí)候在哪個(gè)線程執(zhí)行搅幅,由協(xié)程調(diào)度器所指定阅束,掛起函數(shù)執(zhí)行完之后,又會(huì)重新切回到它原先的線程來(lái)茄唐。這個(gè)就是協(xié)程的優(yōu)勢(shì)所在息裸。
    private fun test3(){
        Log.e("test", "start")
        lifecycleScope.launch {
            launch()
        }
        Log.e("test", "end")
    }
    
    suspend fun launch(){
        Log.e("test", "launch_start")
        delay(3000)
        Log.e("test", "launch_end")
    }

運(yùn)行結(jié)果如下:


image.png

從截圖可以看出,launch函數(shù)被掛起沪编,然后主要流程繼續(xù)執(zhí)行界牡,而launch函數(shù)被掛起后也繼續(xù)執(zhí)行。

  • 掛起函數(shù)線程切換
    從上面看我們已經(jīng)掛起了函數(shù)漾抬,讓程序脫離當(dāng)前的線程,kotlin 協(xié)程提供了一個(gè) withContext() 方法常遂,來(lái)實(shí)現(xiàn)線程切換纳令。在講切線程之前,我們先說(shuō)說(shuō)Dispatchers調(diào)度器克胳,它可以將協(xié)程限制在一個(gè)特定的線程執(zhí)行平绩,或者將它分派到一個(gè)線程池,或者讓它不受限制地運(yùn)行漠另。
Dispatchers調(diào)度器種類
  1. Dispatchers.Main:Android 中的主線程
  2. Dispatchers.IO:針對(duì)磁盤(pán)和網(wǎng)絡(luò) IO 進(jìn)行了優(yōu)化捏雌,適合 IO 密集型的任務(wù),比如:讀寫(xiě)文件笆搓,操作數(shù)據(jù)庫(kù)以及網(wǎng)絡(luò)請(qǐng)求
  3. Dispatchers.Default:適合 CPU 密集型的任務(wù)性湿,比如計(jì)算
  4. Dispatchers.Unconfined:當(dāng)我們不關(guān)心協(xié)程在哪個(gè)線程上被掛起時(shí)使用
    那我們?cè)趺辞袚Q線程呢纬傲,
suspend fun launch() {
    withContext(Dispatchers.IO){
        Log.e("test2", Thread.currentThread().name)
        delay(3000)
        Log.e("test", "launch_end")
    }
}

在lifecycleScope里,就更簡(jiǎn)單了肤频,直接如下

private fun test3() {
    Log.e("test", "start")
    lifecycleScope.launch(Dispatchers.IO) {
        Log.e("test", "launch_start")
        delay(3000)
        withContext(Dispatchers.Main){
            Log.e("test", "launch_end")
        }
    }
    Log.e("test", "end")
}

在協(xié)程里叹括,線程切換就是這么簡(jiǎn)單,在io線程執(zhí)行耗時(shí)任務(wù)宵荒,然后又在main里切會(huì)主線程汁雷。

協(xié)程的取消,追蹤協(xié)程的狀態(tài)

我們開(kāi)啟了一個(gè)協(xié)程报咳,在協(xié)程執(zhí)行期間也想操作這個(gè)協(xié)程侠讯,這就是要用到Job,什么是Job,從概念上講,一個(gè) Job 表示具有生命周期的暑刃、可以取消的東西厢漩。從形態(tài)上將,Job 是一個(gè)接口稍走,但是它有具體的合約和狀態(tài)袁翁,所以它可以被當(dāng)做一個(gè)抽象類來(lái)看待。

Job 一共包含六個(gè)狀態(tài):

  • 新創(chuàng)建 New
  • 活躍 Active
  • 完成中 Completing
  • 已完成 Completed
  • 取消中 Cancelling
  • 已取消 Cancelled

Job 的生命周期會(huì)經(jīng)過(guò)四個(gè)狀態(tài):New → Active → Completing → Completed婿脸。

image.png

下面來(lái)講講job的常用方法:

  • join() 掛起協(xié)程粱胜,直到任務(wù)完成再恢復(fù)
private suspend fun test() {
    Log.e("test", "start")
    job=lifecycleScope.launch(Dispatchers.IO) {
        launch()
    }
    job?.join()
    Log.e("test", "end")
}

 private suspend fun launch() {
    withContext(Dispatchers.IO){
        Log.e("test2", "launch_start")
        delay(3000)
        Log.e("test", "launch_end")
    }
}

運(yùn)行結(jié)果如下:


image.png

可以看到,加上join狐树,協(xié)程代碼會(huì)在這里執(zhí)行焙压,并且是阻塞的,只有執(zhí)行完才會(huì)走下一步

  • cancel() 取消協(xié)程

  • cancelAndJoin() 取消并掛起調(diào)用協(xié)程抑钟,直到被取消的協(xié)程完成

private suspend fun test() {
    Log.e("test", "start")
    job=lifecycleScope.launch(Dispatchers.IO) {
        launch()
    }
    job?.cancelAndJoin()
    Log.e("test", "end")
}

運(yùn)行結(jié)果如下:


image.png

可以看待涯曲,cancelAndJoin(),會(huì)運(yùn)行,然后取消在塔,取消完后會(huì)走下一步

  • start() 如果Job所在的協(xié)程還沒(méi)有被啟動(dòng)那么調(diào)用這個(gè)方法就會(huì)啟動(dòng)協(xié)程,如果這個(gè)協(xié)程被啟動(dòng)了返回true幻件,如果已經(jīng)啟動(dòng)或者執(zhí)行完畢了返回false

代碼如下:

private suspend fun test3() {
    Log.e("test", "start")
    job=lifecycleScope.launch(start = CoroutineStart.LAZY) {
        launch()
    }
    Log.e("test", "end")
    job?.start()
}

運(yùn)行效果如下:


image.png

可以看到當(dāng)設(shè)置延遲加載時(shí),協(xié)程是start()后才開(kāi)始執(zhí)行

說(shuō)到延遲加載蛔溃,在總結(jié)一下協(xié)程啟動(dòng)模式

  • DEFAULT 模式
    默認(rèn)的 協(xié)程啟動(dòng)模式 , 協(xié)程創(chuàng)建后 , 馬上開(kāi)始調(diào)度執(zhí)行 , 如果在 執(zhí)行前或執(zhí)行時(shí) 取消協(xié)程 , 則進(jìn)入 取消響應(yīng) 狀態(tài) ; 如果在執(zhí)行過(guò)程中取消 , 協(xié)程也會(huì)被取消 ;

  • ATOMIC 模式
    協(xié)程創(chuàng)建后 , 馬上開(kāi)始調(diào)度執(zhí)行 , 協(xié)程執(zhí)行到 第一個(gè)掛起點(diǎn) 之前 , 如果取消協(xié)程 , 則不進(jìn)行響應(yīng)取消操作 ;

  • LAZY 模式
    協(xié)程創(chuàng)建后 , 不會(huì)馬上開(kāi)始調(diào)度執(zhí)行 , 只有 主動(dòng)調(diào)用協(xié)程的 start , join , await 方法 時(shí) , 才開(kāi)始調(diào)度執(zhí)行協(xié)程 , 如果在 調(diào)度之前取消協(xié)程 , 該協(xié)程直接報(bào)異常 進(jìn)入異常響應(yīng)狀態(tài) ;

  • UNDISPATCHED 模式
    協(xié)程創(chuàng)建后,立即在當(dāng)前的函數(shù)調(diào)用棧執(zhí)行協(xié)程任務(wù),直到遇到第一個(gè)掛起函數(shù),才在子線程中執(zhí)行掛起函數(shù) ;

    1. 如果在主線程中啟動(dòng)協(xié)程 , 則該模式的協(xié)程就會(huì)直接在主線程中執(zhí)行 ;
    2. 如果在子線程中啟動(dòng)協(xié)程 , 則該模式的協(xié)程就會(huì)直接在子線程中執(zhí)行 ;
協(xié)程異常處理

對(duì)于不同協(xié)程構(gòu)造器绰沥,異常的處理方式不同。分別介紹 launch 和 async 情況下的異常處理

  • Launch
    launch 方式啟動(dòng)的協(xié)程贺待,異常會(huì)在發(fā)生時(shí)立刻拋出徽曲,使用 try catch 就可以將協(xié)程中的異常捕獲。
scope.launch {
    try {
        codeThatCanThrowExceptions()
    } catch(e: Exception) {
        // Handle exception
    }finally{
        //結(jié)束處理
    }
}

也可以try catch整個(gè)協(xié)程

    try {
        scope.launch {
            codeThatCanThrowExceptions()
        }
    } catch(e: Exception) {
        // Handle exception
    }finally{
        //結(jié)束處理
    }
}
  • Async
    當(dāng) async 開(kāi)啟的協(xié)程為根協(xié)程 或 supervisorScope 的直接子協(xié)程時(shí)麸塞,異常在調(diào)用 await 時(shí)拋出秃臣,使用 try catch 可以捕獲異常:
fun main() = runBlocking {
    val deferred = GlobalScope.async {
        throw Exception()
    }
    try {
        deferred.await() //拋出異常
    } catch (t: Throwable) {
        println("捕獲異常:$t")
    }finally{
        //結(jié)束處理
    }
} 
協(xié)程并行

到目前為止,上面的代碼都是串行的哪工,即從上到下依次執(zhí)行奥此,而協(xié)程不單單串行弧哎,我們也可以并行的方式。

  • 使用 async 并發(fā)
private fun test() {
    Log.e("test", "start")
    lifecycleScope.async {
        Log.e("test", "launch1_start")
        delay(1000)
        Log.e("test", "launch1_end")
    }
    lifecycleScope.async {
        Log.e("test", "launch2_start")
        delay(2000)
        Log.e("test", "launch2_end")
    }
    Log.e("test", "end")
}

運(yùn)行效果如下

image.png

可以看到兩個(gè)協(xié)程可以并行執(zhí)行得院,也可以用await()方法傻铣,代碼如下:

private fun test4(){
    lifecycleScope.launch {
        val a=async {
            delay(1000)
            1
        }

        val b=async {
            delay(5000)
            2
        }
        var c=a.await()+b.await()
        Log.e("test",c.toString())
    }
}

通過(guò)await()方法,即使兩個(gè)協(xié)程完成時(shí)間不一致祥绞,最終也可以一起運(yùn)算非洲。

協(xié)程-并發(fā)處理

從上面可以了解到,協(xié)程也是可以并發(fā)的蜕径,既然是并發(fā)两踏,那同樣也會(huì)出現(xiàn)像java多線程并發(fā)的問(wèn)題,導(dǎo)致各種問(wèn)題兜喻,協(xié)程本身也提供了兩種方式處理并發(fā):

  • Mutex
    Mutex 類似于 synchorinzed梦染,協(xié)程競(jìng)爭(zhēng)時(shí)將協(xié)程包裝為 LockWaiter 使用雙向鏈表存儲(chǔ)。Mutex通俗點(diǎn)來(lái)說(shuō)就是kotlin的鎖朴皆,和java 的synchronized和RecentLock對(duì)應(yīng)帕识。

使用mutex.withLock {*} 即可實(shí)現(xiàn)數(shù)據(jù)的同步以簡(jiǎn)化使用,下面給個(gè)事例:
在沒(méi)有加鎖之前:

private fun test(){
    repeat(5) {
        GlobalScope.launch(Dispatchers.IO) {
            delay(2000)
            value++
            Log.e("test",value.toString())
        }
    }
}

開(kāi)啟五個(gè)協(xié)程遂铡,同時(shí)運(yùn)行肮疗,對(duì)value操作:


image.png

可以看到,順序是亂的扒接,而加了mutex之后呢:

var mutex= Mutex()
private fun test(){
    repeat(5) {
        GlobalScope.launch(Dispatchers.IO) {
            delay(2000)
            mutex.withLock {
                value++
                Log.e("test",value.toString())
            }
        }
    }
}

運(yùn)行結(jié)果如下:


image.png

可以看到伪货,加鎖之后,都會(huì)等上一個(gè)運(yùn)行后之后在解鎖钾怔,在運(yùn)行下一個(gè)碱呼。

  • Actors

一個(gè) actor 是由協(xié)程、被限制并封裝到該協(xié)程中的狀態(tài)以及一個(gè)與其它協(xié)程通信的 通道 組合而成的一個(gè)實(shí)體宗侦。一個(gè)簡(jiǎn)單的actor 可以簡(jiǎn)單的寫(xiě)成一個(gè)函數(shù),但是一個(gè)擁有復(fù)雜狀態(tài)的actor更適合由類來(lái)表示愚臀。

有一個(gè) actor 協(xié)程構(gòu)建器,它可以方便地將 actor 的通道組合到其作用域中(用來(lái)接收消息)矾利、組合發(fā)送 channel 與結(jié)果集對(duì)象懊悯,這樣對(duì)actor的單個(gè)引用就可以作為其句柄持有。

使用 actor 的第一步是定義一個(gè) actor 要處理的消息類梦皮。

// 計(jì)數(shù)器 Actor 的各種類型
sealed class CounterMsg
object IncCounter : CounterMsg() // 遞增計(jì)數(shù)器的單向消息
class GetCounter(val response: CompletableDeferred<Int>) : CounterMsg() // 攜帶回復(fù)的請(qǐng)求

接下來(lái)定義一個(gè)函數(shù),使用 actor 協(xié)程構(gòu)建器來(lái)啟動(dòng)一個(gè) actor:

// 這個(gè)函數(shù)啟動(dòng)一個(gè)新的計(jì)數(shù)器 actor
fun CoroutineScope.counterActor() = actor<CounterMsg> {
    var counter = 0 // actor 狀態(tài)
    for (msg in channel) { // 即將到來(lái)消息的迭代器
        when (msg) {
            is IncCounter -> counter++
            is GetCounter -> msg.response.complete(counter)
        }
    }
}

執(zhí)行代碼:

runBlocking {
    val counterActor = counterActor() // 創(chuàng)建該 actor
    repeat(100) {
        launch {
            repeat(1000) {
                counterActor.send(IncCounter)
            }
        }
    }
    delay(3000)
    // 發(fā)送一條消息以用來(lái)從一個(gè) actor 中獲取計(jì)數(shù)值
    val response = CompletableDeferred<Int>()
    counterActor.send(GetCounter(response))
    println("Counter = ${response.await()}")
    counterActor.close() // 關(guān)閉該actor
}

actor 可以修改自己的私有狀態(tài)桃焕,但只能通過(guò)消息互相影響(避免任何鎖定)剑肯。actor 在高負(fù)載下比鎖更有效,因?yàn)樵谶@種情況下它總是有工作要做观堂,而且根本不需要切換到不同的上下文让网,這樣效率更高呀忧。

協(xié)程的創(chuàng)建

寫(xiě)到這里,基本上把協(xié)程的基本用法都說(shuō)了溃睹,最后要用協(xié)程而账,要知道這么創(chuàng)建協(xié)程吧,其實(shí)這里也有分的因篇,所以才放在最后泞辐,假如是單單在kotlin里創(chuàng)建協(xié)程,就有三種方式

  • 使用 runBlocking 頂層函數(shù)創(chuàng)建:
runBlocking {
    ...
}

通常適用于單元測(cè)試的場(chǎng)景竞滓,而業(yè)務(wù)開(kāi)發(fā)中不會(huì)用到這種方法咐吼,因?yàn)樗蔷€程阻塞的。

  • 使用 GlobalScope 單例對(duì)象創(chuàng)建:
GlobalScope.launch {
    ...
}

GlobalScope和使用 runBlocking 的區(qū)別在于不會(huì)阻塞線程商佑。但在 Android 開(kāi)發(fā)中同樣不推薦這種用法,因?yàn)樗纳芷跁?huì)只受整個(gè)應(yīng)用程序的生命周期限制,且不能取消郊闯。

  • 自行通過(guò) CoroutineContext 創(chuàng)建一個(gè) CoroutineScope 對(duì)象:
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
    ...
}

這是比較推薦的使用方法舌涨,我們可以通過(guò) context 參數(shù)去管理和控制協(xié)程的生命周期(這里的 context 和 Android 里的不是一個(gè)東西,是一個(gè)更通用的概念抓半,會(huì)有一個(gè) Android 平臺(tái)的封裝來(lái)配合使用)喂急。

Android平臺(tái)協(xié)程創(chuàng)建

首先需要引用ktx庫(kù)

implementation "androidx.lifecycle:lifecycle-runtime-ktx:版本號(hào)"

這個(gè)時(shí)候我們就可以在activity或者framgent直接使用lifecycleScope進(jìn)行啟動(dòng)協(xié)程。就像我上面的代碼實(shí)例一樣琅关。

lifecycleScope和lifecycle的生命周期一致煮岁,退出的時(shí)候也可以自動(dòng)取消協(xié)程,不用自己手動(dòng)取消涣易。

同時(shí)画机,還擴(kuò)展了lifecycleScope.launchWhenResumed , lifecycleScope.launchWhenCreated 新症,lifecycleScope.launchWhenStarted步氏,
分別對(duì)應(yīng)activity或者fragment的onResumed(),onCreated(),onStarted().

  • viewLifecycleOwner
    雖然fragment也可以用lifecycleScope,但是最好還是viewLifecycleOwner徒爹,因?yàn)镕ragment與Fragment中的View的生命周期并不一致荚醒,需要讓observer感知Fragment中的View的生命周期而非Fragment,
ViewModel中使用協(xié)程

同樣引入擴(kuò)展庫(kù)

implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:版本號(hào)"

引入庫(kù)之后隆嗅,我們就可以在ViewModel用viewModelScope來(lái)使用協(xié)程.

其他環(huán)境下使用協(xié)程

其他情況下的創(chuàng)建按照上面協(xié)程推薦的第三種方式即可

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末界阁,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子胖喳,更是在濱河造成了極大的恐慌泡躯,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異较剃,居然都是意外死亡咕别,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)写穴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)惰拱,“玉大人,你說(shuō)我怎么就攤上這事啊送〕ザ蹋” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵删掀,是天一觀的道長(zhǎng)翔冀。 經(jīng)常有香客問(wèn)我,道長(zhǎng)披泪,這世上最難降的妖魔是什么纤子? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮款票,結(jié)果婚禮上控硼,老公的妹妹穿的比我還像新娘。我一直安慰自己艾少,他們只是感情好卡乾,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著缚够,像睡著了一般幔妨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上谍椅,一...
    開(kāi)封第一講書(shū)人閱讀 51,692評(píng)論 1 305
  • 那天误堡,我揣著相機(jī)與錄音,去河邊找鬼雏吭。 笑死锁施,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的杖们。 我是一名探鬼主播悉抵,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼摘完!你這毒婦竟也來(lái)了姥饰?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤孝治,失蹤者是張志新(化名)和其女友劉穎列粪,沒(méi)想到半個(gè)月后栅螟,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡篱竭,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了步绸。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片掺逼。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖瓤介,靈堂內(nèi)的尸體忽然破棺而出吕喘,到底是詐尸還是另有隱情,我是刑警寧澤刑桑,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布氯质,位于F島的核電站,受9級(jí)特大地震影響祠斧,放射性物質(zhì)發(fā)生泄漏闻察。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一琢锋、第九天 我趴在偏房一處隱蔽的房頂上張望辕漂。 院中可真熱鬧,春花似錦吴超、人聲如沸钉嘹。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)跋涣。三九已至,卻和暖如春鸟悴,著一層夾襖步出監(jiān)牢的瞬間陈辱,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工遣臼, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留性置,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓揍堰,卻偏偏與公主長(zhǎng)得像鹏浅,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子屏歹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355

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