kotlin - Coroutine 協(xié)程

我是在深入學(xué)習(xí) kotlin 時(shí)第一次看到協(xié)程落午,作為傳統(tǒng)線程模型的進(jìn)化版,雖說(shuō)協(xié)程這個(gè)概念幾十年前就有了蜕便,但是協(xié)程只是在近年才開始興起,應(yīng)用的語(yǔ)言有: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é)程

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

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

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

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

協(xié)程的開發(fā)人員 Roman Elizarov 是這樣描述協(xié)程的:協(xié)程就像非常輕量級(jí)的線程栖茉。線程是由系統(tǒng)調(diào)度的,線程切換或線程阻塞的開銷都比較大孵延。而協(xié)程依賴于線程吕漂,但是協(xié)程掛起時(shí)不需要阻塞線程,幾乎是無(wú)代價(jià)的尘应,協(xié)程是由開發(fā)者控制的惶凝。所以協(xié)程也像用戶態(tài)的線程吼虎,非常輕量級(jí),一個(gè)線程中可以創(chuàng)建任意個(gè)協(xié)程苍鲜。

Coroutine思灰,翻譯成”協(xié)程“,初始碰到的人馬上就會(huì)跟進(jìn)程和線程兩個(gè)概念聯(lián)系起來(lái)混滔。直接先說(shuō)區(qū)別洒疚,Coroutine是編譯器級(jí)的,Process和Thread是操作系統(tǒng)級(jí)的坯屿。Coroutine的實(shí)現(xiàn)油湖,通常是對(duì)某個(gè)語(yǔ)言做相應(yīng)的提議,然后通過(guò)后成編譯器標(biāo)準(zhǔn)领跛,然后編譯器廠商來(lái)實(shí)現(xiàn)該機(jī)制乏德。Process和Thread看起來(lái)也在語(yǔ)言層次,但是內(nèi)生原理卻是操作系統(tǒng)先有這個(gè)東西吠昭,然后通過(guò)一定的API暴露給用戶使用喊括,兩者在這里有不同。Process和Thread是os通過(guò)調(diào)度算法矢棚,保存當(dāng)前的上下文瘾晃,然后從上次暫停的地方再次開始計(jì)算,重新開始的地方不可預(yù)期幻妓,每次CPU計(jì)算的指令數(shù)量和代碼跑過(guò)的CPU時(shí)間是相關(guān)的蹦误,跑到os分配的cpu時(shí)間到達(dá)后就會(huì)被os強(qiáng)制掛起。Coroutine是編譯器的魔術(shù)肉津,通過(guò)插入相關(guān)的代碼使得代碼段能夠?qū)崿F(xiàn)分段式的執(zhí)行强胰,重新開始的地方是yield關(guān)鍵字指定的,一次一定會(huì)跑到一個(gè)yield對(duì)應(yīng)的地方

對(duì)于多線程應(yīng)用妹沙,CPU通過(guò)切片的方式來(lái)切換線程間的執(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é)程和線程的對(duì)比:

  • Thread - 線程擁有獨(dú)立的棧、局部變量,基于進(jìn)程的共享內(nèi)存玉凯,因此數(shù)據(jù)共享比較容易势腮,但是多線程時(shí)需要加鎖來(lái)進(jìn)行訪問(wèn)控制,不加鎖就容易導(dǎo)致數(shù)據(jù)錯(cuò)誤漫仆,但加鎖過(guò)多又容易出現(xiàn)死鎖捎拯。線程之間的調(diào)度由內(nèi)核控制(時(shí)間片競(jìng)爭(zhēng)機(jī)制),程序員無(wú)法介入控制(即便我們擁有sleep盲厌、yield這樣的API署照,這些API只是看起來(lái)像,但本質(zhì)還是交給內(nèi)核去控制狸眼,我們最多就是加上幾個(gè)條件控制罷了)藤树,線程之間的切換需要深入到內(nèi)核級(jí)別浴滴,因此線程的切換代價(jià)比較大拓萌,表現(xiàn)在:

    • 線程對(duì)象的創(chuàng)建和初始化
    • 線程上下文切換
    • 線程狀態(tài)的切換由系統(tǒng)內(nèi)核完成
    • 對(duì)變量的操作需要加鎖
    image
  • Coroutine 協(xié)程是跑在線程上的優(yōu)化產(chǎn)物,被稱為輕量級(jí) Thread升略,擁有自己的棧內(nèi)存和局部變量微王,共享成員變量。傳統(tǒng) Thread 執(zhí)行的核心是一個(gè)while(true) 的函數(shù)品嚣,本質(zhì)就是一個(gè)耗時(shí)函數(shù)炕倘,Coroutine 可以用來(lái)直接標(biāo)記方法,由程序員自己實(shí)現(xiàn)切換翰撑,調(diào)度罩旋,不再采用傳統(tǒng)的時(shí)間段競(jìng)爭(zhēng)機(jī)制。在一個(gè)線程上可以同時(shí)跑多個(gè)協(xié)程眶诈,同一時(shí)間只有一個(gè)協(xié)程被執(zhí)行涨醋,在單線程上模擬多線程并發(fā),協(xié)程何時(shí)運(yùn)行逝撬,何時(shí)暫停浴骂,都是有程序員自己決定的,使用: yield/resume API宪潮,優(yōu)勢(shì)如下:

    • 因?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ù)

      image

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

拿 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的過(guò)程中蜗帜,可以隨時(shí)中斷,去執(zhí)行B资厉,B也可能在執(zhí)行過(guò)程中中斷再去執(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 支持庫(kù)更新為 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é)程的描述窥妇,下面來(lái)自官方文檔:

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

總結(jié)下,協(xié)程是跑在線程上的穆趴,一個(gè)線程可以同時(shí)跑多個(gè)協(xié)程脸爱,每一個(gè)協(xié)程則代表一個(gè)耗時(shí)任務(wù),我們手動(dòng)控制多個(gè)協(xié)程之間的運(yùn)行未妹、切換簿废,決定誰(shuí)什么時(shí)候掛起,什么時(shí)候運(yùn)行络它,什么時(shí)候喚醒族檬,而不是 Thread 那樣交給系統(tǒng)內(nèi)核來(lái)操作去競(jìng)爭(zhēng) 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é)程的掛起請(qǐng)求后,會(huì)去執(zhí)行其他計(jì)算任務(wù)沉颂,比如其他協(xié)程条摸。協(xié)程通過(guò)這樣的手段來(lái)實(shí)現(xiàn)多線程、異步的效果铸屉,在思維邏輯上同 Thread 的確有比較大的區(qū)別钉蒲,大家需要適應(yīng)下思路上的變化


suspend 關(guān)鍵字

協(xié)程天然親近方法,協(xié)程表現(xiàn)為標(biāo)記彻坛、切換方法顷啼、代碼段,協(xié)程里使用 suspend 關(guān)鍵字修飾方法小压,既該方法可以被協(xié)程掛起线梗,沒(méi)用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 語(yǔ)句即可
    }
}


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

kotlin 里沒(méi)有 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

來(lái)看一個(gè)最簡(jiǎn)單的例子:

    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è)對(duì)象式函數(shù)涡贱,這樣的話 kotlin 就可以使用以前說(shuō)的閉包的寫法:() 里面寫常規(guī)參數(shù),{} 里面寫函數(shù)式對(duì)象的實(shí)現(xiàn)惹想,就像上面的例子一樣问词,剛從 java 轉(zhuǎn)過(guò)來(lái)的朋友看著很別扭不是,得適應(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 - 沒(méi)指定海雪,就是在當(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é)程對(duì)象本身,協(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()}")

image

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 通常的用法是用來(lái)橋接普通阻塞代碼和掛起風(fēng)格的非阻塞代碼杰赛,在 runBlocking 閉包里面啟動(dòng)另外的協(xié)程呢簸,協(xié)程里面是可以嵌套啟動(dòng)別的協(xié)程的


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

前面說(shuō)過(guò),協(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 線程之間采取的是競(jìng)爭(zhēng) cpu 時(shí)間段的方法,誰(shuí)搶到誰(shuí)運(yùn)行召川,由系統(tǒng)內(nèi)核控制南缓,對(duì)我們來(lái)說(shuō)是不可見不可控的。協(xié)程不同荧呐,協(xié)程之間不用競(jìng)爭(zhēng)汉形、誰(shuí)運(yùn)行纸镊、誰(shuí)掛起、什么時(shí)候恢復(fù)都是由我們自己控制的

最簡(jiǎn)單的協(xié)程運(yùn)行模式概疆,不涉及掛起時(shí)逗威,誰(shuí)寫在前面誰(shuí)先運(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()}")
}

image

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

大家還記得 suspend 這個(gè)關(guān)鍵字嗎差凹,suspend 表示掛起的意思,用來(lái)修飾方法的侧馅,一個(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)
}

image

在 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)
}

image

注意我外面要包裹一層 GlobalScope.launch,要不運(yùn)行不了呐萨。這里我們搞了2個(gè)協(xié)程出來(lái)杀饵,但是我們?cè)谶@里使用了await,這樣就會(huì)阻塞外部協(xié)程谬擦,所以代碼還是按順序執(zhí)行的切距。這樣適用于多個(gè)同級(jí) IO 操作的情況,這樣寫比 rxjava 要省事不少

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

這個(gè)問(wèn)題值得我們研究惨远,畢竟代碼運(yùn)行是負(fù)載的谜悟,協(xié)程之外線程里肯定還有需要執(zhí)行的代碼饵沧,我們來(lái)看看前面的代碼在掛起后何時(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()}")
}

image

協(xié)程掛起后缅叠,雖然延遲的時(shí)間到了悄泥,但是還得等到線程空閑時(shí)才能繼續(xù)執(zhí)行,這里要注意肤粱,協(xié)程可沒(méi)有競(jìng)爭(zhēng) cpu 時(shí)間段弹囚,協(xié)程掛起后即便可以恢復(fù)執(zhí)行了也不是馬上就能恢復(fù)執(zhí)行,需要我們自己結(jié)合上下文代碼去判斷领曼,這里寫不好是要出問(wèn)題的

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

為什么要寫這個(gè)呢鸥鹉,在 Thread 中不存在這個(gè)問(wèn)題,但是協(xié)程中有句話這樣說(shuō)的:哪個(gè)線程恢復(fù)的協(xié)程庶骄,協(xié)程就運(yùn)行在哪個(gè)線程中毁渗,我們分別對(duì) kotlin 提供的 3個(gè)協(xié)程調(diào)度器測(cè)試一下。我們用這段代碼測(cè)試单刁,分別設(shè)置 3個(gè)協(xié)程調(diào)度器

GlobalScope.launch(Dispatchers.Main){
  Log.d("AA", "協(xié)程測(cè)試 開始執(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

    image

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

  • Dispatchers.Unconfined

    image

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

  • Dispatchers.IO

    image

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

注意協(xié)程內(nèi)部锦募,若是在前面有代碼切換了線程,后面的代碼若是沒(méi)有指定線程邻遏,那么就是運(yùn)行在這個(gè)切換到的線程上的糠亩,所以大家看上面的測(cè)試結(jié)果虐骑,setText 執(zhí)行的線程都和上一個(gè)方法一樣

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

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é)程測(cè)試 開始執(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)
    }

image

5\. delay赏殃、yield 區(qū)別

delay 和 yield 方法是協(xié)程內(nèi)部的操作灰嫉,可以掛起協(xié)程,區(qū)別是 delay 是掛起協(xié)程并經(jīng)過(guò)執(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í)間過(guò)后會(huì)放到隊(duì)列里去册赛,沒(méi)有延遲時(shí)間的直接放到隊(duì)列里去

6\. 協(xié)程的取消

我們?cè)趧?chuàng)建協(xié)程過(guò)后可以接受一個(gè) Job 類型的返回值,我們操作 job 可以取消協(xié)程任務(wù)震嫉,job.cancel 就可以了

        // 協(xié)程任務(wù)
        job = GlobalScope.launch(Dispatchers.IO) {
            Log.d("AA", "協(xié)程測(cè)試 開始執(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)在問(wèn)題來(lái)了窗宇,在 Thread 中我們想關(guān)閉線程有時(shí)候也不是掉個(gè)方法就行的措伐,需要我們自行在線程中判斷縣城是不是已經(jīng)結(jié)束了。在協(xié)程中一樣军俊,cancel 方法只是修改了協(xié)程的狀態(tài)侥加,在協(xié)程自身的方法比如 realy,yield 等中會(huì)判斷協(xié)程的狀態(tài)從而結(jié)束協(xié)程粪躬,但是若是在協(xié)程我們沒(méi)有用這幾個(gè)方法怎么辦担败,比如都是邏輯代碼,這時(shí)就要我們自己手動(dòng)判斷了短蜕,使用 job.isActive 氢架,isActive 是個(gè)標(biāo)記,用來(lái)檢查協(xié)程狀態(tài)


其他內(nèi)容

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

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

線程中鎖都是阻塞式岖研,在沒(méi)有獲取鎖時(shí)無(wú)法執(zhí)行其他邏輯,而協(xié)程可以通過(guò)掛起函數(shù)解決這個(gè)警检,沒(méi)有獲取鎖就掛起協(xié)程孙援,獲取后再恢復(fù)協(xié)程,協(xié)程掛起時(shí)線程并沒(méi)有阻塞可以執(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é)程請(qǐng)求網(wǎng)絡(luò)數(shù)據(jù)

我們用帶返回值的協(xié)程 GlobalScope.async 在 IO 線程中去執(zhí)行網(wǎng)絡(luò)請(qǐng)求础淤,然后通過(guò) await 返回請(qǐng)求結(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ò)請(qǐng)求
                // 放回了請(qǐng)求后的結(jié)構(gòu)
                return@async "main"
            }.await()
        }
    }
}

轉(zhuǎn)自:http://www.reibang.com/p/76d2f47b900d
其他相關(guān)文章:http://www.reibang.com/p/2659bbe0df16

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末鸽凶,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子建峭,更是在濱河造成了極大的恐慌玻侥,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件亿蒸,死亡現(xiàn)場(chǎng)離奇詭異凑兰,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)边锁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門姑食,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人砚蓬,你說(shuō)我怎么就攤上這事矢门。” “怎么了灰蛙?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵祟剔,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我摩梧,道長(zhǎng)物延,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任仅父,我火速辦了婚禮叛薯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘笙纤。我一直安慰自己耗溜,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布省容。 她就那樣靜靜地躺著抖拴,像睡著了一般。 火紅的嫁衣襯著肌膚如雪腥椒。 梳的紋絲不亂的頭發(fā)上阿宅,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音笼蛛,去河邊找鬼洒放。 笑死,一個(gè)胖子當(dāng)著我的面吹牛滨砍,可吹牛的內(nèi)容都是我干的往湿。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼惋戏,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼领追!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起日川,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤蔓腐,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后龄句,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體回论,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年分歇,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了傀蓉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡职抡,死狀恐怖葬燎,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤谱净,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布窑邦,位于F島的核電站,受9級(jí)特大地震影響壕探,放射性物質(zhì)發(fā)生泄漏冈钦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一李请、第九天 我趴在偏房一處隱蔽的房頂上張望瞧筛。 院中可真熱鬧,春花似錦导盅、人聲如沸较幌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)乍炉。三九已至,卻和暖如春嘁字,著一層夾襖步出監(jiān)牢的瞬間恩急,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工纪蜒, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留衷恭,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓纯续,卻偏偏與公主長(zhǎng)得像随珠,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子猬错,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345