我是在深入學(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)作是要鉆研一下的,上手可能不是那么快
- 官方中文文檔:kotlin 中文文檔
- 簡友資料庫:JohnnyShieh
這里有個(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è)核
-
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)莽鸿。Yield
、resume
這一對 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 這個(gè)調(diào)度器在協(xié)程掛起后會(huì)一直跑在主線程上,但是有一點(diǎn)注意啊蹈矮,主線程中寫在程后面的代碼先執(zhí)行了砰逻,這就有點(diǎn)坑了,要注意啊含滴,Dispatchers.Main 是以給主線程 handle 添加任務(wù)的方式現(xiàn)實(shí)在主線程上的運(yùn)行的 -
Dispatchers.Unconfined 在首次掛起之后再恢復(fù)運(yùn)行诱渤,所在線程已經(jīng)不是首次運(yùn)行時(shí)的主線程了,而是默認(rèn)線程池中的線程谈况,這里要特別注意啊勺美,看來協(xié)程的喚醒不是那么簡單的,協(xié)程內(nèi)部做了很多工作 -
看來 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)容
- 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)用
- 協(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()
}
}
}