kotlin - Coroutine 協(xié)程

我是在深入學(xué)習(xí) kotlin 時(shí)第一次看到協(xié)程,作為傳統(tǒng)線程模型的進(jìn)化版此改,雖說協(xié)程這個(gè)概念幾十年前就有了趾撵,但是協(xié)程只是在近年才開始興起,應(yīng)用的語言有:go 共啃、goLand占调、kotlin、python , 都是支持協(xié)程的移剪,可能不同平臺(tái) API 上有差異

首次學(xué)習(xí)協(xié)程可能會(huì)費(fèi)些時(shí)間究珊,協(xié)程和 thread 類似,但是和 thread 有很大區(qū)別纵苛,搞懂剿涮,學(xué)會(huì)言津,熟悉協(xié)程在線程上如何運(yùn)作是要鉆研一下的,上手可能不是那么快

這里有個(gè)很有建設(shè)性意見的例子取试,Coroutines 代替 rxjava


怎么理解協(xié)程

我們先來把協(xié)程這個(gè)概念搞懂悬槽,不是很好理解,但是也不難理解

協(xié)程 - 也叫微線程瞬浓,是一種新的多任務(wù)并發(fā)的操作手段(也不是很新初婆,概念早就有了)

  • 特征:協(xié)程是運(yùn)行在單線程中的并發(fā)程序
  • 優(yōu)點(diǎn):省去了傳統(tǒng) Thread 多線程并發(fā)機(jī)制中切換線程時(shí)帶來的線程上下文切換、線程狀態(tài)切換猿棉、Thread 初始化上的性能損耗磅叛,能大幅度唐提高并發(fā)性能
  • 漫畫版概念解釋:漫畫:什么是協(xié)程?
  • 簡單理解:在單線程上由程序員自己調(diào)度運(yùn)行的并行計(jì)算

下面是關(guān)于協(xié)程這個(gè)概念的一些描述:

協(xié)程的開發(fā)人員 Roman Elizarov 是這樣描述協(xié)程的:協(xié)程就像非常輕量級(jí)的線程萨赁。線程是由系統(tǒng)調(diào)度的弊琴,線程切換或線程阻塞的開銷都比較大。而協(xié)程依賴于線程位迂,但是協(xié)程掛起時(shí)不需要阻塞線程访雪,幾乎是無代價(jià)的,協(xié)程是由開發(fā)者控制的掂林。所以協(xié)程也像用戶態(tài)的線程臣缀,非常輕量級(jí),一個(gè)線程中可以創(chuàng)建任意個(gè)協(xié)程泻帮。

Coroutine精置,翻譯成”協(xié)程“,初始碰到的人馬上就會(huì)跟進(jìn)程和線程兩個(gè)概念聯(lián)系起來锣杂。直接先說區(qū)別脂倦,Coroutine是編譯器級(jí)的,Process和Thread是操作系統(tǒng)級(jí)的元莫。Coroutine的實(shí)現(xiàn)赖阻,通常是對某個(gè)語言做相應(yīng)的提議,然后通過后成編譯器標(biāo)準(zhǔn)踱蠢,然后編譯器廠商來實(shí)現(xiàn)該機(jī)制火欧。Process和Thread看起來也在語言層次,但是內(nèi)生原理卻是操作系統(tǒng)先有這個(gè)東西茎截,然后通過一定的API暴露給用戶使用苇侵,兩者在這里有不同。Process和Thread是os通過調(diào)度算法企锌,保存當(dāng)前的上下文榆浓,然后從上次暫停的地方再次開始計(jì)算,重新開始的地方不可預(yù)期撕攒,每次CPU計(jì)算的指令數(shù)量和代碼跑過的CPU時(shí)間是相關(guān)的陡鹃,跑到os分配的cpu時(shí)間到達(dá)后就會(huì)被os強(qiáng)制掛起烘浦。Coroutine是編譯器的魔術(shù),通過插入相關(guān)的代碼使得代碼段能夠?qū)崿F(xiàn)分段式的執(zhí)行杉适,重新開始的地方是yield關(guān)鍵字指定的谎倔,一次一定會(huì)跑到一個(gè)yield對應(yīng)的地方

對于多線程應(yīng)用柳击,CPU通過切片的方式來切換線程間的執(zhí)行猿推,線程切換時(shí)需要耗時(shí)(保存狀態(tài),下次繼續(xù))捌肴。協(xié)程蹬叭,則只使用一個(gè)線程,在一個(gè)線程中規(guī)定某個(gè)代碼塊執(zhí)行順序状知。協(xié)程能保留上一次調(diào)用時(shí)的狀態(tài)秽五,不需要像線程一樣用回調(diào)函數(shù),所以性能上會(huì)有提升饥悴。缺點(diǎn)是本質(zhì)是個(gè)單線程坦喘,不能利用到單個(gè)CPU的多個(gè)核

協(xié)程和線程的對比:

  • Thread - 線程擁有獨(dú)立的棧、局部變量西设,基于進(jìn)程的共享內(nèi)存瓣铣,因此數(shù)據(jù)共享比較容易,但是多線程時(shí)需要加鎖來進(jìn)行訪問控制贷揽,不加鎖就容易導(dǎo)致數(shù)據(jù)錯(cuò)誤棠笑,但加鎖過多又容易出現(xiàn)死鎖。線程之間的調(diào)度由內(nèi)核控制(時(shí)間片競爭機(jī)制)禽绪,程序員無法介入控制(即便我們擁有sleep蓖救、yield這樣的API,這些API只是看起來像印屁,但本質(zhì)還是交給內(nèi)核去控制循捺,我們最多就是加上幾個(gè)條件控制罷了),線程之間的切換需要深入到內(nèi)核級(jí)別雄人,因此線程的切換代價(jià)比較大从橘,表現(xiàn)在:
    * 線程對象的創(chuàng)建和初始化
    * 線程上下文切換
    * 線程狀態(tài)的切換由系統(tǒng)內(nèi)核完成
    * 對變量的操作需要加鎖

  • Coroutine 協(xié)程是跑在線程上的優(yōu)化產(chǎn)物,被稱為輕量級(jí) Thread柠衍,擁有自己的棧內(nèi)存和局部變量洋满,共享成員變量。傳統(tǒng) Thread 執(zhí)行的核心是一個(gè)while(true) 的函數(shù)珍坊,本質(zhì)就是一個(gè)耗時(shí)函數(shù)牺勾,Coroutine 可以用來直接標(biāo)記方法,由程序員自己實(shí)現(xiàn)切換阵漏,調(diào)度驻民,不再采用傳統(tǒng)的時(shí)間段競爭機(jī)制翻具。在一個(gè)線程上可以同時(shí)跑多個(gè)協(xié)程,同一時(shí)間只有一個(gè)協(xié)程被執(zhí)行回还,在單線程上模擬多線程并發(fā)裆泳,協(xié)程何時(shí)運(yùn)行,何時(shí)暫停柠硕,都是有程序員自己決定的工禾,使用: yield/resume API,優(yōu)勢如下:

    • 因?yàn)樵谕粋€(gè)線程里蝗柔,協(xié)程之間的切換不涉及線程上下文的切換和線程狀態(tài)的改變闻葵,不存在資源、數(shù)據(jù)并發(fā)癣丧,所以不用加鎖槽畔,只需要判斷狀態(tài)就OK,所以執(zhí)行效率比多線程高很多
    • 協(xié)程是非阻塞式的(也有阻塞API)胁编,一個(gè)協(xié)程在進(jìn)入阻塞后不會(huì)阻塞當(dāng)前線程厢钧,當(dāng)前線程會(huì)去執(zhí)行其他協(xié)程任務(wù)


程序員能夠控制協(xié)程的切換,是通過yield API 讓協(xié)程在空閑時(shí)(比如等待io嬉橙,網(wǎng)絡(luò)數(shù)據(jù)未到達(dá))放棄執(zhí)行權(quán)早直,然后在合適的時(shí)機(jī)再通過resume API 喚醒協(xié)程繼續(xù)運(yùn)行。協(xié)程一旦開始運(yùn)行就不會(huì)結(jié)束憎夷,直到遇到yield交出執(zhí)行權(quán)莽鸿。Yieldresume 這一對 API 可以非常便捷的實(shí)現(xiàn)異步拾给,這可是目前所有高級(jí)語法孜孜不倦追求的

拿 python 代碼舉個(gè)例子祥得,在一個(gè)線程里運(yùn)行下面2個(gè)方法:

def A():
    print '1'
    print '2'
    print '3'

def B():
    print 'x'
    print 'y'
    print 'z'

假設(shè)由協(xié)程執(zhí)行,每個(gè)方法都用協(xié)程標(biāo)記蒋得,在執(zhí)行A的過程中级及,可以隨時(shí)中斷,去執(zhí)行B额衙,B也可能在執(zhí)行過程中中斷再去執(zhí)行A饮焦,結(jié)果可能是:1 2 x y 3 z


添加依賴

在 module 項(xiàng)目中添加下面的依賴:

    kotlin{
        experimental {
            coroutines 'enable'
        }
    }

    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'

kotlin Coroutine 部分在最近幾個(gè)版本變化較大,推薦大家使用 kotlin 的最新版本 1.3.21窍侧,同時(shí) kotlin 1.3.21 版本 kotlin-stdlib-jre7 支持庫更新為 kotlin-stdlib-jdk7

buildscript {
    ext.kotlin_version = '1.3.21'
    ......
}
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

kotlin 種協(xié)程的概念

文章開頭已經(jīng)介紹了協(xié)程的概念县踢,但畢竟平臺(tái)不同,也許多協(xié)程也有不一樣的地方伟件,我們還是看看 kotlin 中的協(xié)程的描述硼啤,下面來自官方文檔:

協(xié)程通過將復(fù)雜性放入庫來簡化異步編程。程序的邏輯可以在協(xié)程中順序地表達(dá)斧账,而底層庫會(huì)為我們解決其異步性谴返。該庫可以將用戶代碼的相關(guān)部分包裝為回調(diào)煞肾、訂閱相關(guān)事件、在不同線程(甚至不同機(jī)器)上調(diào)度執(zhí)行嗓袱,而代碼則保持如同順序執(zhí)行一樣簡單

總結(jié)下籍救,協(xié)程是跑在線程上的,一個(gè)線程可以同時(shí)跑多個(gè)協(xié)程渠抹,每一個(gè)協(xié)程則代表一個(gè)耗時(shí)任務(wù)蝙昙,我們手動(dòng)控制多個(gè)協(xié)程之間的運(yùn)行、切換逼肯,決定誰什么時(shí)候掛起耸黑,什么時(shí)候運(yùn)行桃煎,什么時(shí)候喚醒篮幢,而不是 Thread 那樣交給系統(tǒng)內(nèi)核來操作去競爭 CPU 時(shí)間片

協(xié)程在線程中是順序執(zhí)行的,既然是順序執(zhí)行的那怎么實(shí)現(xiàn)異步为迈,這自然是有手段的三椿。Thread 中我們有阻塞、喚醒的概念葫辐,協(xié)程里同樣也有搜锰,掛起等同于阻塞,區(qū)別是 Thread 的阻塞是會(huì)阻塞當(dāng)前線程的(此時(shí)線程只能空耗 cpu 時(shí)間而不能執(zhí)行其他計(jì)算任務(wù)耿战,是種浪費(fèi))蛋叼,而協(xié)程的掛起不會(huì)阻塞線程。當(dāng)線程接收到某個(gè)協(xié)程的掛起請求后剂陡,會(huì)去執(zhí)行其他計(jì)算任務(wù)狈涮,比如其他協(xié)程。協(xié)程通過這樣的手段來實(shí)現(xiàn)多線程鸭栖、異步的效果歌馍,在思維邏輯上同 Thread 的確有比較大的區(qū)別,大家需要適應(yīng)下思路上的變化


suspend 關(guān)鍵字

協(xié)程天然親近方法晕鹊,協(xié)程表現(xiàn)為標(biāo)記松却、切換方法、代碼段溅话,協(xié)程里使用 suspend 關(guān)鍵字修飾方法晓锻,既該方法可以被協(xié)程掛起,沒用suspend修飾的方法不能參與協(xié)程任務(wù)飞几,suspend修飾的方法只能在協(xié)程中只能與另一個(gè)suspend修飾的方法交流

suspend fun requestToken(): Token { ... }   // 掛起函數(shù)
suspend fun createPost(token: Token, item: Item): Post { ... }  // 掛起函數(shù)

fun postItem(item: Item) {
    GlobalScope.launch { // 創(chuàng)建一個(gè)新協(xié)程
        val token = requestToken()
        val post = createPost(token, item)
        processPost(post)
        // 需要異常處理砚哆,直接加上 try/catch 語句即可
    }
}

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

kotlin 里沒有 new ,自然也不像 JAVA 一樣 new Thread循狰,另外 kotlin 里面提供了大量的高階函數(shù)窟社,所以不難猜出協(xié)程這里 kotlin 也是有提供專用函數(shù)的券勺。kotlin 中 GlobalScope 類提供了幾個(gè)攜程構(gòu)造函數(shù):

  • launch - 創(chuàng)建協(xié)程
  • async - 創(chuàng)建帶返回值的協(xié)程,返回的是 Deferred 類
  • withContext - 不創(chuàng)建新的協(xié)程灿里,在指定協(xié)程上運(yùn)行代碼塊
  • runBlocking - 不是 GlobalScope 的 API关炼,可以獨(dú)立使用,區(qū)別是 runBlocking 里面的 delay 會(huì)阻塞線程匣吊,而 launch 創(chuàng)建的不會(huì)

kotlin 在 1.3 之后要求協(xié)程必須由 CoroutineScope 創(chuàng)建儒拂,CoroutineScope 不阻塞當(dāng)前線程,在后臺(tái)創(chuàng)建一個(gè)新協(xié)程色鸳,也可以指定協(xié)程調(diào)度器社痛。比如 CoroutineScope.launch{} 可以看成 new Coroutine

來看一個(gè)最簡單的例子:

    Log.d("AA", "協(xié)程初始化開始,時(shí)間: " + System.currentTimeMillis())

    GlobalScope.launch(Dispatchers.Unconfined) {
        Log.d("AA", "協(xié)程初始化完成命雀,時(shí)間: " + System.currentTimeMillis())
        for (i in 1..3) {
            Log.d("AA", "協(xié)程任務(wù)1打印第$i 次蒜哀,時(shí)間: " + System.currentTimeMillis())
        }
        delay(500)
        for (i in 1..3) {
            Log.d("AA", "協(xié)程任務(wù)2打印第$i 次,時(shí)間: " + System.currentTimeMillis())
        }
    }

    Log.d("AA", "主線程 sleep 吏砂,時(shí)間: " + System.currentTimeMillis())
    Thread.sleep(1000)
    Log.d("AA", "主線程運(yùn)行撵儿,時(shí)間: " + System.currentTimeMillis())

    for (i in 1..3) {
        Log.d("AA", "主線程打印第$i 次,時(shí)間: " + System.currentTimeMillis())
    }
協(xié)程初始化開始狐血,時(shí)間: 1553752816027
協(xié)程初始化完成淀歇,時(shí)間: 1553752816060
協(xié)程任務(wù)1打印第1 次,時(shí)間: 1553752816060
協(xié)程任務(wù)1打印第2 次匈织,時(shí)間: 1553752816060
協(xié)程任務(wù)1打印第3 次浪默,時(shí)間: 1553752816060
主線程 sleep 类少,時(shí)間: 1553752816063
協(xié)程任務(wù)2打印第1 次菠发,時(shí)間: 1553752816567
協(xié)程任務(wù)2打印第2 次,時(shí)間: 1553752816567
協(xié)程任務(wù)2打印第3 次忿薇,時(shí)間: 1553752816567
主線程運(yùn)行弦追,時(shí)間: 1553752817067
主線程打印第1 次岳链,時(shí)間: 1553752817068
主線程打印第2 次,時(shí)間: 1553752817068
主線程打印第3 次劲件,時(shí)間: 1553752817068

以 launch 函數(shù)為例

launch 函數(shù)定義:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job

launch 是個(gè)擴(kuò)展函數(shù)掸哑,接受3個(gè)參數(shù),前面2個(gè)是常規(guī)參數(shù)零远,最后一個(gè)是個(gè)對象式函數(shù)苗分,這樣的話 kotlin 就可以使用以前說的閉包的寫法:() 里面寫常規(guī)參數(shù),{} 里面寫函數(shù)式對象的實(shí)現(xiàn)牵辣,就像上面的例子一樣摔癣,剛從 java 轉(zhuǎn)過來的朋友看著很別扭不是,得適應(yīng)

 GlobalScope.launch(Dispatchers.Unconfined) {...}

我們需要關(guān)心的是 launch 的3個(gè)參數(shù)和返回值 Job:

  • CoroutineContext - 可以理解為協(xié)程的上下文,在這里我們可以設(shè)置 CoroutineDispatcher 協(xié)程運(yùn)行的線程調(diào)度器择浊,有 4種線程模式:
    • Dispatchers.Default
    • Dispatchers.IO -
    • Dispatchers.Main - 主線程
    • Dispatchers.Unconfined - 沒指定戴卜,就是在當(dāng)前線程

不寫的話就是 Dispatchers.Default 模式的,或者我們可以自己創(chuàng)建協(xié)程上下文琢岩,也就是線程池投剥,newSingleThreadContext 單線程,newFixedThreadPoolContext 線程池担孔,具體的可以點(diǎn)進(jìn)去看看江锨,這2個(gè)都是方法

val singleThreadContext = newSingleThreadContext("aa")
GlobalScope.launch(singleThreadContext) { ... }
  • CoroutineStart - 啟動(dòng)模式,默認(rèn)是DEAFAULT糕篇,也就是創(chuàng)建就啟動(dòng)啄育;還有一個(gè)是LAZY,意思是等你需要它的時(shí)候拌消,再調(diào)用啟動(dòng)
    • DEAFAULT - 模式模式挑豌,不寫就是默認(rèn)
    • ATOMIC -
    • UNDISPATCHED
    • LAZY - 懶加載模式,你需要它的時(shí)候拼坎,再調(diào)用啟動(dòng)浮毯,看這個(gè)例子
var job:Job = GlobalScope.launch( start = CoroutineStart.LAZY ){
    Log.d("AA", "協(xié)程開始運(yùn)行,時(shí)間: " + System.currentTimeMillis())
}

Thread.sleep( 1000L )
// 手動(dòng)啟動(dòng)協(xié)程
job.start()
  • block - 閉包方法體泰鸡,定義協(xié)程內(nèi)需要執(zhí)行的操作
  • Job - 協(xié)程構(gòu)建函數(shù)的返回值,可以把 Job 看成協(xié)程對象本身壳鹤,協(xié)程的操作方法都在 Job 身上了
    • job.start() - 啟動(dòng)協(xié)程盛龄,除了 lazy 模式,協(xié)程都不需要手動(dòng)啟動(dòng)
    • job.join() - 等待協(xié)程執(zhí)行完畢
    • job.cancel() - 取消一個(gè)協(xié)程
    • job.cancelAndJoin() - 等待協(xié)程執(zhí)行完畢然后再取消

GlobalScope.async

async 同 launch 唯一的區(qū)別就是 async 是有返回值的芳誓,看下面的例子:

GlobalScope.launch(Dispatchers.Unconfined) {
  val deferred = GlobalScope.async{
  delay(1000L)
  Log.d("AA","This is async ")
  return@async "taonce"
  }

  Log.d("AA","協(xié)程 other start")
  val result = deferred.await()
  Log.d("AA","async result is $result")
  Log.d("AA","協(xié)程 other end ")
}

Log.d("AA", "主線程位于協(xié)程之后的代碼執(zhí)行余舶,時(shí)間:  ${System.currentTimeMillis()}")

async 返回的是 Deferred 類型,Deferred 繼承自 Job 接口锹淌,Job有的它都有匿值,增加了一個(gè)方法 await ,這個(gè)方法接收的是 async 閉包中返回的值赂摆,async 的特點(diǎn)是不會(huì)阻塞當(dāng)前線程挟憔,但會(huì)阻塞所在協(xié)程,也就是掛起

但是注意啊烟号,async 并不會(huì)阻塞線程绊谭,只是阻塞鎖調(diào)用的協(xié)程


runBlocking

runBlocking 和 launch 區(qū)別的地方就是 runBlocking 的 delay 方法是可以阻塞當(dāng)前的線程的,和Thread.sleep() 一樣汪拥,看下面的例子:

fun main(args: Array<String>) {
  runBlocking {
    // 阻塞1s
    delay(1000L)
    println("This is a coroutines ${TimeUtil.getTimeDetail()}")
  }

  // 阻塞2s
  Thread.sleep(2000L)
  println("main end ${TimeUtil.getTimeDetail()}")
  }

~~~~~~~~~~~~~~log~~~~~~~~~~~~~~~~
This is a coroutines 11:00:51
main end 11:00:53

runBlocking 通常的用法是用來橋接普通阻塞代碼和掛起風(fēng)格的非阻塞代碼达传,在 runBlocking 閉包里面啟動(dòng)另外的協(xié)程,協(xié)程里面是可以嵌套啟動(dòng)別的協(xié)程的


協(xié)程的掛起和恢復(fù)

前面說過,協(xié)程的特點(diǎn)就是多個(gè)協(xié)程可以運(yùn)行在一個(gè)線程內(nèi)宪赶,單個(gè)協(xié)程掛起后不會(huì)阻塞當(dāng)前線程宗弯,線程還可以繼續(xù)執(zhí)行其他任務(wù)。學(xué)習(xí)協(xié)程最大的難點(diǎn)是搞清楚協(xié)程是如何運(yùn)行的搂妻、何時(shí)掛起罕伯、何時(shí)恢復(fù),多個(gè)協(xié)程之間的組織運(yùn)行叽讳、協(xié)程和線程之間的組織運(yùn)行

1. 協(xié)程執(zhí)行時(shí)追他, 協(xié)程和協(xié)程,協(xié)程和線程內(nèi)代碼是順序運(yùn)行的

這點(diǎn)是和 thread 最大的不同岛蚤,thread 線程之間采取的是競爭 cpu 時(shí)間段的方法邑狸,誰搶到誰運(yùn)行,由系統(tǒng)內(nèi)核控制涤妒,對我們來說是不可見不可控的单雾。協(xié)程不同,協(xié)程之間不用競爭她紫、誰運(yùn)行硅堆、誰掛起、什么時(shí)候恢復(fù)都是由我們自己控制的

最簡單的協(xié)程運(yùn)行模式贿讹,不涉及掛起時(shí)渐逃,誰寫在前面誰先運(yùn)行,后面的等前面的協(xié)程運(yùn)行完之后再運(yùn)行民褂。涉及到掛起時(shí)茄菊,前面的協(xié)程掛起了,那么線程不會(huì)空閑赊堪,而是繼續(xù)運(yùn)行下一個(gè)協(xié)程面殖,而前面掛起的那個(gè)協(xié)程在掛起結(jié)速后不會(huì)馬上運(yùn)行,而是等待當(dāng)前正在運(yùn)行的協(xié)程運(yùn)行完畢后再去執(zhí)行

典型的例子:

GlobalScope.launch(Dispatchers.Unconfined) {
  for (i in 1..6) {
    Log.d("AA", "協(xié)程任務(wù)打印第$i 次哭廉,時(shí)間: ${System.currentTimeMillis()}")
  }
}

  for (i in 1..8) {
  Log.d("AA", "主線程打印第$i 次脊僚,時(shí)間:  ${System.currentTimeMillis()}")
}

2. 協(xié)程掛起時(shí),就不會(huì)執(zhí)行了遵绰,而是等待掛起完成且線程空閑時(shí)才能繼續(xù)執(zhí)行

大家還記得 suspend 這個(gè)關(guān)鍵字嗎辽幌,suspend 表示掛起的意思,用來修飾方法的街立,一個(gè)協(xié)程內(nèi)有多個(gè) suspend 修飾的方法順序書寫時(shí)舶衬,代碼也是順序運(yùn)行的,為什么赎离,suspend 函數(shù)會(huì)將整個(gè)協(xié)程掛起逛犹,而不僅僅是這個(gè) suspend 函數(shù)

  • 1. 單攜程內(nèi)多 suspend 函數(shù)運(yùn)行
    suspend 修飾的方法掛起的是協(xié)程本身,而非該方法,注意這點(diǎn)虽画,看下面的代碼體會(huì)下
suspend fun getToken(): String {
  delay(300)
  Log.d("AA", "getToken 開始執(zhí)行舞蔽,時(shí)間:  ${System.currentTimeMillis()}")
  return "ask"
}

suspend fun getResponse(token: String): String {
  delay(100)
  Log.d("AA", "getResponse 開始執(zhí)行,時(shí)間:  ${System.currentTimeMillis()}")
  return "response"
}

fun setText(response: String) {
  Log.d("AA", "setText 執(zhí)行码撰,時(shí)間:  ${System.currentTimeMillis()}")
}

// 運(yùn)行代碼
GlobalScope.launch(Dispatchers.Main) {
  Log.d("AA", "協(xié)程 開始執(zhí)行渗柿,時(shí)間:  ${System.currentTimeMillis()}")
  val token = getToken()
  val response = getResponse(token)
  setText(response)
}

在 getToken 方法將協(xié)程掛起時(shí),getResponse 函數(shù)永遠(yuǎn)不會(huì)運(yùn)行脖岛,只有等 getToken 掛起結(jié)速將協(xié)程恢復(fù)時(shí)才會(huì)運(yùn)行

  • 2. 多協(xié)程間 suspend 函數(shù)運(yùn)行
GlobalScope.launch(Dispatchers.Unconfined){
  var token = GlobalScope.async(Dispatchers.Unconfined) {
    return@async getToken()
   }.await()

  var response = GlobalScope.async(Dispatchers.Unconfined) {
    return@async getResponse(token)
  }.await()

  setText(response)
}

注意我外面要包裹一層 GlobalScope.launch,要不運(yùn)行不了门扇。這里我們搞了2個(gè)協(xié)程出來吉拳,但是我們在這里使用了await涡扼,這樣就會(huì)阻塞外部協(xié)程什猖,所以代碼還是按順序執(zhí)行的摇零。這樣適用于多個(gè)同級(jí) IO 操作的情況登渣,這樣寫比 rxjava 要省事不少

3. 協(xié)程掛起后何時(shí)恢復(fù)

這個(gè)問題值得我們研究,畢竟代碼運(yùn)行是負(fù)載的带到,協(xié)程之外線程里肯定還有需要執(zhí)行的代碼狭握,我們來看看前面的代碼在掛起后何時(shí)才能恢復(fù)執(zhí)行。我們把上面的方法延遲改成 1ms ,2ms

suspend fun getToken(): String {
  delay(1)
  Log.d("AA", "getToken 開始執(zhí)行腾仅,時(shí)間:  ${System.currentTimeMillis()}")
  return "ask"
}

suspend fun getResponse(token: String): String {
  delay(2)
  Log.d("AA", "getResponse 開始執(zhí)行惰蜜,時(shí)間:  ${System.currentTimeMillis()}")
  return "response"
}

fun setText(response: String) {
  Log.d("AA", "setText 執(zhí)行,時(shí)間:  ${System.currentTimeMillis()}")
}

GlobalScope.launch(Dispatchers.Unconfined) {
  Log.d("AA", "協(xié)程 開始執(zhí)行醉拓,時(shí)間:  ${System.currentTimeMillis()}")

  val token = getToken()
  val response = getResponse(token)

  setText(response)
}

for (i in 1..10) {
  Log.d("AA", "主線程打印第$i 次钻哩,時(shí)間:  ${System.currentTimeMillis()}")
}

協(xié)程掛起后,雖然延遲的時(shí)間到了近范,但是還得等到線程空閑時(shí)才能繼續(xù)執(zhí)行蔗喂,這里要注意,協(xié)程可沒有競爭 cpu 時(shí)間段缰儿,協(xié)程掛起后即便可以恢復(fù)執(zhí)行了也不是馬上就能恢復(fù)執(zhí)行畦粮,需要我們自己結(jié)合上下文代碼去判斷,這里寫不好是要出問題的

4. 協(xié)程掛起后再恢復(fù)時(shí)在哪個(gè)線程運(yùn)行

為什么要寫這個(gè)呢乖阵,在 Thread 中不存在這個(gè)問題宣赔,但是協(xié)程中有句話這樣說的:哪個(gè)線程恢復(fù)的協(xié)程,協(xié)程就運(yùn)行在哪個(gè)線程中瞪浸,我們分別對 kotlin 提供的 3個(gè)協(xié)程調(diào)度器測試一下儒将。我們用這段代碼測試,分別設(shè)置 3個(gè)協(xié)程調(diào)度器

GlobalScope.launch(Dispatchers.Main){
  Log.d("AA", "協(xié)程測試 開始執(zhí)行对蒲,線程:${Thread.currentThread().name}")

  var token = GlobalScope.async(Dispatchers.Unconfined) {
    return@async getToken()
  }.await()

  var response = GlobalScope.async(Dispatchers.Unconfined) {
    return@async getResponse(token)
  }.await()

  setText(response)
}

Log.d("AA", "主線程協(xié)程后面代碼執(zhí)行钩蚊,線程:${Thread.currentThread().name}")
  • Dispatchers.Main


    看來 Dispatchers.Main 這個(gè)調(diào)度器在協(xié)程掛起后會(huì)一直跑在主線程上,但是有一點(diǎn)注意啊蹈矮,主線程中寫在程后面的代碼先執(zhí)行了砰逻,這就有點(diǎn)坑了,要注意啊含滴,Dispatchers.Main 是以給主線程 handle 添加任務(wù)的方式現(xiàn)實(shí)在主線程上的運(yùn)行的

  • Dispatchers.Unconfined


    Dispatchers.Unconfined 在首次掛起之后再恢復(fù)運(yùn)行诱渤,所在線程已經(jīng)不是首次運(yùn)行時(shí)的主線程了,而是默認(rèn)線程池中的線程谈况,這里要特別注意啊勺美,看來協(xié)程的喚醒不是那么簡單的,協(xié)程內(nèi)部做了很多工作

  • Dispatchers.IO


    看來 Dispatchers.IO 這個(gè)調(diào)度器在協(xié)程掛起后碑韵,也是切到默認(rèn)線程池去了赡茸,不過最后又切回最開始的 IO 線程了

注意協(xié)程內(nèi)部,若是在前面有代碼切換了線程祝闻,后面的代碼若是沒有指定線程占卧,那么就是運(yùn)行在這個(gè)切換到的線程上的,所以大家看上面的測試結(jié)果联喘,setText 執(zhí)行的線程都和上一個(gè)方法一樣

我們最好給異步任務(wù)在外面套一個(gè)協(xié)程华蜒,這樣我們可以掛掛起整個(gè)異步任務(wù),然后給每段代碼指定運(yùn)行線程調(diào)度器豁遭,這樣省的因?yàn)閰f(xié)程內(nèi)部掛起恢復(fù)變更線程而帶來的問題

Kotlin Coroutines封裝異步回調(diào)叭喜、協(xié)程間關(guān)系及協(xié)程的取消 一文中,作者分析了源碼蓖谢,非 Dispatchers.Main 調(diào)度器的協(xié)程捂蕴,會(huì)在協(xié)程掛起后把協(xié)程當(dāng)做一個(gè)任務(wù) DelayedResumeTask 放到默認(rèn)線程池 DefaultExecutor 隊(duì)列的最后譬涡,在延遲的時(shí)間到達(dá)才會(huì)執(zhí)行恢復(fù)協(xié)程任務(wù)。雖然多個(gè)協(xié)程之間可能不是在同一個(gè)線程上運(yùn)行的啥辨,但是協(xié)程內(nèi)部的機(jī)制可以保證我們書寫的協(xié)程是按照我們指定的順序或者邏輯自行

看個(gè)例子:

    suspend fun getToken(): String {
        Log.d("AA", "getToken start涡匀,線程:${Thread.currentThread().name}")
        delay(100)
        Log.d("AA", "getToken end,線程:${Thread.currentThread().name}")
        return "ask"
    }

    suspend fun getResponse(token: String): String {
        Log.d("AA", "getResponse start溉知,線程:${Thread.currentThread().name}")
        delay(200)
        Log.d("AA", "getResponse end陨瘩,線程:${Thread.currentThread().name}")
        return "response"
    }

    fun setText(response: String) {
        Log.d("AA", "setText 執(zhí)行,線程:${Thread.currentThread().name}")
    }

    // 實(shí)際運(yùn)行
    GlobalScope.launch(Dispatchers.IO) {
        Log.d("AA", "協(xié)程測試 開始執(zhí)行着倾,線程:${Thread.currentThread().name}")
        var token = GlobalScope.async(Dispatchers.IO) {
            return@async getToken()
        }.await()

        var response = GlobalScope.async(Dispatchers.IO) {
            return@async getResponse(token)
        }.await()

        setText(response)
    }

5. relay拾酝、yield 區(qū)別

relay 和 yield 方法是協(xié)程內(nèi)部的操作,可以掛起協(xié)程卡者,區(qū)別是 relay 是掛起協(xié)程并經(jīng)過執(zhí)行時(shí)間恢復(fù)協(xié)程蒿囤,當(dāng)線程空閑時(shí)就會(huì)運(yùn)行協(xié)程;yield 是掛起協(xié)程崇决,讓協(xié)程放棄本次 cpu 執(zhí)行機(jī)會(huì)讓給別的協(xié)程材诽,當(dāng)線程空閑時(shí)再次運(yùn)行協(xié)程。我們只要使用 kotlin 提供的協(xié)程上下文類型恒傻,線程池是有多個(gè)線程的脸侥,再次執(zhí)行的機(jī)會(huì)很快就會(huì)有的。

除了 main 類型盈厘,協(xié)程在掛起后都會(huì)封裝成任務(wù)放到協(xié)程默認(rèn)線程池的任務(wù)隊(duì)列里去睁枕,有延遲時(shí)間的在時(shí)間過后會(huì)放到隊(duì)列里去,沒有延遲時(shí)間的直接放到隊(duì)列里去

6. 協(xié)程的取消

我們在創(chuàng)建協(xié)程過后可以接受一個(gè) Job 類型的返回值沸手,我們操作 job 可以取消協(xié)程任務(wù)外遇,job.cancel 就可以了

        // 協(xié)程任務(wù)
        job = GlobalScope.launch(Dispatchers.IO) {
            Log.d("AA", "協(xié)程測試 開始執(zhí)行,線程:${Thread.currentThread().name}")
            var token = GlobalScope.async(Dispatchers.IO) {
                return@async getToken()
            }.await()

            var response = GlobalScope.async(Dispatchers.IO) {
                return@async getResponse(token)
            }.await()

            setText(response)
        }
      
        // 取消協(xié)程
        job?.cancel()
        Log.d("AA", "btn_right 結(jié)束協(xié)程")

協(xié)程的取消有些特質(zhì)契吉,因?yàn)閰f(xié)程內(nèi)部可以在創(chuàng)建協(xié)程的跳仿,這樣的協(xié)程組織關(guān)系可以稱為父協(xié)程,子協(xié)程:

  • 父協(xié)程手動(dòng)調(diào)用 cancel() 或者異常結(jié)束捐晶,會(huì)立即取消它的所有子協(xié)程
  • 父協(xié)程必須等待所有子協(xié)程完成(處于完成或者取消狀態(tài))才能完成
  • 子協(xié)程拋出未捕獲的異常時(shí)菲语,默認(rèn)情況下會(huì)取消其父協(xié)程

現(xiàn)在問題來了,在 Thread 中我們想關(guān)閉線程有時(shí)候也不是掉個(gè)方法就行的惑灵,需要我們自行在線程中判斷縣城是不是已經(jīng)結(jié)束了山上。在協(xié)程中一樣,cancel 方法只是修改了協(xié)程的狀態(tài)英支,在協(xié)程自身的方法比如 realy胶哲,yield 等中會(huì)判斷協(xié)程的狀態(tài)從而結(jié)束協(xié)程,但是若是在協(xié)程我們沒有用這幾個(gè)方法怎么辦潭辈,比如都是邏輯代碼鸯屿,這時(shí)就要我們自己手動(dòng)判斷了,使用 job.isActive 把敢,isActive 是個(gè)標(biāo)記寄摆,用來檢查協(xié)程狀態(tài)


其他內(nèi)容

我也是初次學(xué)習(xí)使用協(xié)程,這里放一些暫時(shí)沒高徹底的內(nèi)容

  1. Mutex 協(xié)程互斥鎖

線程中鎖都是阻塞式修赞,在沒有獲取鎖時(shí)無法執(zhí)行其他邏輯婶恼,而協(xié)程可以通過掛起函數(shù)解決這個(gè),沒有獲取鎖就掛起協(xié)程柏副,獲取后再恢復(fù)協(xié)程勾邦,協(xié)程掛起時(shí)線程并沒有阻塞可以執(zhí)行其他邏輯。這種互斥鎖就是 Mutex割择,它與 synchronized 關(guān)鍵字有些類似眷篇,還提供了 withLock 擴(kuò)展函數(shù),替代常用的 mutex.lock; try {...} finally { mutex.unlock() }

更多的使用經(jīng)驗(yàn)就要大家自己取找找了

fun main(args: Array<String>) = runBlocking<Unit> {
    val mutex = Mutex()
    var counter = 0
    repeat(10000) {
        GlobalScope.launch {
            mutex.withLock {
                counter ++
            }
        }
    }
    println("The final count is $counter")
}

協(xié)程應(yīng)用

  1. 協(xié)程請求網(wǎng)絡(luò)數(shù)據(jù)

我們用帶返回值的協(xié)程 GlobalScope.async 在 IO 線程中去執(zhí)行網(wǎng)絡(luò)請求荔泳,然后通過 await 返回請求結(jié)果蕉饼,用launch 在主線程中更新UI就行了,注意外面用 runBlocking 包裹

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        coroutine.setOnClickListener { click() }
    }

    private fun click() = runBlocking {
        GlobalScope.launch(Dispatchers.Main) {
            coroutine.text = GlobalScope.async(Dispatchers.IO) {
                // 比如進(jìn)行了網(wǎng)絡(luò)請求
                // 放回了請求后的結(jié)構(gòu)
                return@async "main"
            }.await()
        }
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末玛歌,一起剝皮案震驚了整個(gè)濱河市昧港,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌支子,老刑警劉巖创肥,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異值朋,居然都是意外死亡叹侄,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門吞歼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來圈膏,“玉大人,你說我怎么就攤上這事篙骡』ぃ” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵糯俗,是天一觀的道長尿褪。 經(jīng)常有香客問我,道長得湘,這世上最難降的妖魔是什么杖玲? 我笑而不...
    開封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮淘正,結(jié)果婚禮上摆马,老公的妹妹穿的比我還像新娘臼闻。我一直安慰自己,他們只是感情好囤采,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開白布述呐。 她就那樣靜靜地躺著,像睡著了一般蕉毯。 火紅的嫁衣襯著肌膚如雪乓搬。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天代虾,我揣著相機(jī)與錄音进肯,去河邊找鬼。 笑死棉磨,一個(gè)胖子當(dāng)著我的面吹牛江掩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播含蓉,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼频敛,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了馅扣?” 一聲冷哼從身側(cè)響起斟赚,我...
    開封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎差油,沒想到半個(gè)月后拗军,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蓄喇,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年发侵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片妆偏。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡刃鳄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出钱骂,到底是詐尸還是另有隱情叔锐,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布见秽,位于F島的核電站愉烙,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏解取。R本人自食惡果不足惜步责,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蔓肯,春花似錦遂鹊、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至气忠,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間赋咽,已是汗流浹背旧噪。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留脓匿,地道東北人淘钟。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像陪毡,于是被迫代替她去往敵國和親米母。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

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