協(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ù),以同步方式寫異步代碼。

  • 掛起: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ì)磁盤和網(wǎng)絡(luò) IO 進(jìn)行了優(yōu)化,適合 IO 密集型的任務(wù),比如:讀寫文件,操作數(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)

我們開啟了一個(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()后才開始執(zhí)行

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

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

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

  • LAZY 模式
    協(xié)程創(chuàng)建后 , 不會(huì)馬上開始調(diào)度執(zhí)行 , 只有 主動(dòng)調(diào)用協(xié)程的 start , join , await 方法 時(shí) , 才開始調(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 開啟的協(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())
        }
    }
}

開啟五個(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)單的寫成一個(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é)程的基本用法都說(shuō)了,最后要用協(xié)程,要知道這么創(chuàng)建協(xié)程吧,其實(shí)這里也有分的,所以才放在最后,假如是單單在kotlin里創(chuàng)建協(xié)程,就有三種方式

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

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

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

GlobalScope和使用 runBlocking 的區(qū)別在于不會(huì)阻塞線程。但在 Android 開發(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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容