為什么要搞出和用協(xié)程呢
是節(jié)省CPU霞怀,避免系統(tǒng)內(nèi)核級的線程頻繁切換,造成的CPU資源浪費(fèi)莉给。好鋼用在刀刃上毙石。而協(xié)程是用戶態(tài)的線程,用戶可以自行控制協(xié)程的創(chuàng)建于銷毀颓遏,極大程度避免了系統(tǒng)級線程上下文切換造成的資源浪費(fèi)徐矩。
是節(jié)約內(nèi)存,在64位的Linux中州泊,一個(gè)線程需要分配8MB棧內(nèi)存和64MB堆內(nèi)存丧蘸,系統(tǒng)內(nèi)存的制約導(dǎo)致我們無法開啟更多線程實(shí)現(xiàn)高并發(fā)。而在協(xié)程編程模式下,可以輕松有十幾萬協(xié)程力喷,這是線程無法比擬的刽漂。
是穩(wěn)定性,前面提到線程之間通過內(nèi)存來共享數(shù)據(jù)弟孟,這也導(dǎo)致了一個(gè)問題贝咙,任何一個(gè)線程出錯(cuò)時(shí),進(jìn)程中的所有線程都會跟著一起崩潰拂募。
是開發(fā)效率庭猩,使用協(xié)程在開發(fā)程序之中,可以很方便的將一些耗時(shí)的IO操作異步化陈症,例如寫文件蔼水、耗時(shí)IO請求等。
對于協(xié)程的一個(gè)總結(jié)
特征:協(xié)程是運(yùn)行在單線程中的并發(fā)程序
優(yōu)點(diǎn):省去了傳統(tǒng) Thread 多線程并發(fā)機(jī)制中切換線程時(shí)帶來的線程上下文切換录肯、線程狀態(tài)切換趴腋、Thread 初始化上的性能損耗,能大幅度的提高并發(fā)性能
簡單理解:在單線程上由程序員自己調(diào)度運(yùn)行的并行計(jì)算
寫到最后
協(xié)程本身不是替換線程的,因?yàn)閰f(xié)程是建立在線程之上的,但是協(xié)程能夠更好的為我們提供執(zhí)行高并發(fā)任務(wù)
1.kotlin中協(xié)程的特點(diǎn):可以用同步的方式寫出異步的代碼
coroutineScope.launch(Dispatchers.Main){// 開始協(xié)程:主線程
? ? val token=api.getToken()// 網(wǎng)絡(luò)請求:IO 線程
? ? val user=api.getUser(token)// 網(wǎng)絡(luò)請求:IO 線程
? ? nameTv.text=user.name// 更新 UI:主線程
}
2.協(xié)程中掛起的本質(zhì)
啟動一個(gè)協(xié)程可以使用 launch 或者 async 函數(shù)论咏,協(xié)程其實(shí)就是這兩個(gè)函數(shù)中閉包的代碼塊优炬。
launch ,async 或者其他函數(shù)創(chuàng)建的協(xié)程厅贪,在執(zhí)行到某一個(gè) suspend 函數(shù)的時(shí)候蠢护,這個(gè)協(xié)程會被「suspend」,也就是被掛起养涮。
3.協(xié)程的代碼塊中葵硕,線程執(zhí)行到了 suspend 函數(shù)這里的時(shí)候,就暫時(shí)不再執(zhí)行剩余的協(xié)程代碼单寂,跳出協(xié)程的代碼塊贬芥。
那線程接下來會做什么呢?
如果它是一個(gè)后臺線程:
要么無事可做宣决,被系統(tǒng)回收
要么繼續(xù)執(zhí)行別的后臺任務(wù)
跟 Java 線程池里的線程在工作結(jié)束之后是完全一樣的:回收或者再利用蘸劈。
如果這個(gè)線程它是 Android 的主線程,那它接下來就會繼續(xù)回去工作:也就是一秒鐘 60 次的界面刷新任務(wù)尊沸。
一個(gè)常見的場景是威沫,獲取一個(gè)圖片,然后顯示出來:
// 主線程中
GlobalScope.launch(Dispatchers.Main){
? ? valimage=suspendingGetImage(imageId)// 獲取圖片
? ? avatarIv.setImageBitmap(image)// 顯示出來
}
suspend fun suspendingGetImage(id:String)=withContext(Dispatchers.IO){...}
協(xié)程:
線程的代碼在到達(dá)suspend函數(shù)的時(shí)候被掐斷洼专,接下來協(xié)程會從這個(gè)suspend函數(shù)開始繼續(xù)往下執(zhí)行棒掠,不過是在指定的線程。
誰指定的屁商?是suspend函數(shù)指定的烟很,比如我們這個(gè)例子中,函數(shù)內(nèi)部的withContext傳入的Dispatchers.IO所指定的 IO 線程。
Dispatchers調(diào)度器雾袱,它可以將協(xié)程限制在一個(gè)特定的線程執(zhí)行恤筛,或者將它分派到一個(gè)線程池,或者讓它不受限制地運(yùn)行芹橡,關(guān)于Dispatchers這里先不展開了毒坛。
那我們平日里常用到的調(diào)度器有哪些?
常用的Dispatchers林说,有以下三種:
Dispatchers.Main:Android 中的主線程
Dispatchers.IO:針對磁盤和網(wǎng)絡(luò) IO 進(jìn)行了優(yōu)化煎殷,適合 IO 密集型的任務(wù),比如:讀寫文件腿箩,操作數(shù)據(jù)庫以及網(wǎng)絡(luò)請求
Dispatchers.Default:適合 CPU 密集型的任務(wù)豪直,比如計(jì)算
回到我們的協(xié)程,它從suspend函數(shù)開始脫離啟動它的線程度秘,繼續(xù)執(zhí)行在Dispatchers所指定的 IO 線程顶伞。
緊接著在suspend函數(shù)執(zhí)行完成之后饵撑,協(xié)程為我們做的最爽的事就來了:會自動幫我們把線程再切回來剑梳。
這個(gè)「切回來」是什么意思?
我們的協(xié)程原本是運(yùn)行在主線程的滑潘,當(dāng)代碼遇到 suspend 函數(shù)的時(shí)候垢乙,發(fā)生線程切換,根據(jù)Dispatchers切換到了 IO 線程语卤;
當(dāng)這個(gè)函數(shù)執(zhí)行完畢后追逮,線程又切了回來,「切回來」也就是協(xié)程會幫我再post一個(gè)Runnable粹舵,讓我剩下的代碼繼續(xù)回到主線程去執(zhí)行钮孵。
我們從線程和協(xié)程的兩個(gè)角度都分析完成后,終于可以對協(xié)程的「掛起」suspend 做一個(gè)解釋:
協(xié)程在執(zhí)行到有 suspend 標(biāo)記的函數(shù)的時(shí)候眼滤,會被 suspend 也就是被掛起巴席,而所謂的被掛起,就是切個(gè)線程诅需;
不過區(qū)別在于漾唉,掛起函數(shù)在執(zhí)行完成之后,協(xié)程會重新切回它原先的線程堰塌。
再簡單來講赵刑,在 Kotlin 中所謂的掛起,就是一個(gè)稍后會被自動切回來的線程調(diào)度操作场刑。
這個(gè)「切回來」的動作般此,在 Kotlin 里叫做 resume,恢復(fù)。
通過剛才的分析我們知道:掛起之后是需要恢復(fù)铐懊。
而恢復(fù)這個(gè)功能是協(xié)程的屎勘,如果你不在協(xié)程里面調(diào)用,恢復(fù)這個(gè)功能沒法實(shí)現(xiàn)居扒,所以也就回答了這個(gè)問題:為什么掛起函數(shù)必須在協(xié)程或者另一個(gè)掛起函數(shù)里被調(diào)用概漱。
再細(xì)想下這個(gè)邏輯:一個(gè)掛起函數(shù)要么在協(xié)程里被調(diào)用,要么在另一個(gè)掛起函數(shù)里被調(diào)用喜喂,那么它其實(shí)直接或者間接地瓤摧,總是會在一個(gè)協(xié)程里被調(diào)用的。
所以玉吁,要求suspend函數(shù)只能在協(xié)程里或者另一個(gè) suspend 函數(shù)里被調(diào)用照弥,還是為了要讓協(xié)程能夠在suspend函數(shù)切換線程之后再切回
通過剛才的分析我們知道:掛起之后是需要恢復(fù)。
而恢復(fù)這個(gè)功能是協(xié)程的进副,如果你不在協(xié)程里面調(diào)用这揣,恢復(fù)這個(gè)功能沒法實(shí)現(xiàn),所以也就回答了這個(gè)問題:為什么掛起函數(shù)必須在協(xié)程或者另一個(gè)掛起函數(shù)里被調(diào)用影斑。
再細(xì)想下這個(gè)邏輯:一個(gè)掛起函數(shù)要么在協(xié)程里被調(diào)用给赞,要么在另一個(gè)掛起函數(shù)里被調(diào)用,那么它其實(shí)直接或者間接地矫户,總是會在一個(gè)協(xié)程里被調(diào)用的片迅。
所以,要求suspend函數(shù)只能在協(xié)程里或者另一個(gè) suspend 函數(shù)里被調(diào)用皆辽,還是為了要讓協(xié)程能夠在 suspend 函數(shù)切換線程之后再切回來柑蛇。
什么是「非阻塞式掛起」
非阻塞式是相對阻塞式而言的。
編程語言中的很多概念其實(shí)都來源于生活驱闷,就像脫口秀的段子一樣耻台。
線程阻塞很好理解,現(xiàn)實(shí)中的例子就是交通堵塞空另,它的核心有 3 點(diǎn):
前面有障礙物盆耽,你過不去(線程卡了)
需要等障礙物清除后才能過去(耗時(shí)任務(wù)結(jié)束)
除非你繞道而行(切到別的線程)
從語義上理解「非阻塞式掛起」,講的是「非阻塞式」這個(gè)是掛起的一個(gè)特點(diǎn)痹换,也就是說征字,協(xié)程的掛起,就是非阻塞式的娇豫,協(xié)程是不講「阻塞式的掛起」的概念的匙姜。
我們講「非阻塞式掛起」,其實(shí)它有幾個(gè)前提:并沒有限定在一個(gè)線程里說這件事冯痢,因?yàn)閽炱疬@件事氮昧,本來就是涉及到多線程框杜。
就像視頻里講的,阻塞不阻塞袖肥,都是針對單線程講的咪辱,一旦切了線程,肯定是非阻塞的椎组,你都跑到別的線程了油狂,之前的線程就自由了,可以繼續(xù)做別的事情了寸癌。
所以「非阻塞式掛起」专筷,其實(shí)就是在講協(xié)程在掛起的同時(shí)切線程這件事情。
為什么要講非阻塞式掛起
「非阻塞式掛起」和第二篇的「掛起要切線程」是同一件事情蒸苇,那還有講的必要嗎磷蛹?
是有的。因?yàn)樗趯懛ㄉ虾蛦尉€程的阻塞式是一樣的溪烤。
協(xié)程只是在寫法上「看起來阻塞」味咳,其實(shí)是「非阻塞」的,因?yàn)樵趨f(xié)程里面它做了很多工作檬嘀,其中有一個(gè)就是幫我們切線程槽驶。
之前說的掛起,重點(diǎn)是說切線程先切過去枪眉,然后再切回來捺檬。
而這里的非阻塞式,重點(diǎn)是說線程雖然會切贸铜,但寫法上和普通的單線程差不多。
讓我們來看看下面的例子:
main{
GlobalScope.launch(Dispatchers.Main){// 耗時(shí)操作val user=suspendingRequestUser()
updateView(user)
}
private suspend fun suspendingRequestUser():User=withContext(Dispatchers.IO){api.requestUser()}}
阻塞的本質(zhì)
首先聂受,所有的代碼本質(zhì)上都是阻塞式的蒿秦,而只有比較耗時(shí)的代碼才會導(dǎo)致人類可感知的等待,比如在主線程上做一個(gè)耗時(shí) 50 ms 的操作會導(dǎo)致界面卡掉幾幀蛋济,這種是我們?nèi)搜勰苡^察出來的棍鳖,而這就是我們通常意義所說的「阻塞」。
舉個(gè)例子碗旅,當(dāng)你開發(fā)的 app 在性能好的手機(jī)上很流暢渡处,在性能差的老手機(jī)上會卡頓,就是在說同一行代碼執(zhí)行的時(shí)間不一樣祟辟。
視頻中講了一個(gè)網(wǎng)絡(luò) IO 的例子医瘫,IO 阻塞更多是反映在「等」這件事情上,它的性能瓶頸是和網(wǎng)絡(luò)的數(shù)據(jù)交換旧困,你切多少個(gè)線程都沒用醇份,該花的時(shí)間一點(diǎn)都少不了稼锅。
而這跟協(xié)程半毛錢關(guān)系沒有,切線程解決不了的事情僚纷,協(xié)程也解決不了
總結(jié)
關(guān)于這邊文章的標(biāo)題協(xié)程是什么矩距、掛起是什么、掛起的非阻塞式可以做下面的總結(jié)
協(xié)程是什么
協(xié)程就是切線程怖竭;
掛起是什么
掛起就是可以自動切回來的切線程锥债;
掛起的非阻塞式
掛起的非阻塞式指的是它能用看起來阻塞的代碼寫出非阻塞的操作。
2.在kotlin使用協(xié)程
項(xiàng)目中配置對 Kotlin 協(xié)程的支持
在使用協(xié)程之前痊臭,我們需要在 build.gradle 文件中增加對 Kotlin 協(xié)程的依賴:
項(xiàng)目根目錄下的 build.gradle :
buildscript {
? ? ext.kotlin_coroutines = '1.4.0'
}
Module 下的 build.gradle
dependencies {
? ? ? ? implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0'
}
創(chuàng)建協(xié)程
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 會阻塞線程,而 launch 創(chuàng)建的不會
先跑起來一個(gè)簡單的例子:
import kotlinx.coroutines.*
fun main(){
? ? ? ? GlobalScope.launch{
? ? ? ? // 在后臺啟動一個(gè)新的協(xié)程并繼續(xù)
? ? ? ? delay(1000L)// 非阻塞的等待 1 秒鐘(默認(rèn)時(shí)間單是毫秒)
? ? ? ? println("World!")// 在延遲后打印輸出}
? ? ? ? println("Hello,")// 協(xié)程已在等待時(shí)主線程還在繼續(xù)
? ? ? ? Thread.sleep(2000L)// 阻塞主線程 2 秒鐘來保證 JVM 存活
}
協(xié)程中卻有一個(gè)很實(shí)用的函數(shù):withContext 艇潭。這個(gè)函數(shù)可以切換到指定的線程拼窥,并在閉包內(nèi)的邏輯執(zhí)行結(jié)束之后,自動把線程切回去繼續(xù)執(zhí)行蹋凝。那么可以將上面的代碼寫成這樣:
coroutineScope.launch(
? ? Dispatchers.Main){//? 在 UI 線程開始
? ? val image=withContext(Dispatchers.IO){// 切換到 IO 線程鲁纠,并在執(zhí)行完成后切回 UI 線程
? ? getImage(imageId)// 將會運(yùn)行在 IO 線程}
? ? avatarIv.setImageBitmap(image)// 回到 UI 線程更新 UI
}
我們甚至可以把 withContext 放進(jìn)一個(gè)單獨(dú)的函數(shù)里面:
launch(Dispatchers.Main){//? 在 UI 線程開始
val image=getImage(imageId)
avatarIv.setImageBitmap(image)//? 執(zhí)行結(jié)束后,自動切換回 UI 線程}//
suspend fun getImage(imageId:Int)=withContext(Dispatchers.IO){...}
launch 函數(shù)
launch 函數(shù)定義
public fun CoroutineScope.launch(
? ? context:CoroutineContext=EmptyCoroutineContext,
? ? start:CoroutineStart=CoroutineStart.DEFAULT,
? ? block:suspendCoroutineScope.()->Unit):Job
launch 是個(gè)擴(kuò)展函數(shù)鳍寂,接受3個(gè)參數(shù)改含,前面2個(gè)是常規(guī)參數(shù),最后一個(gè)是個(gè)對象式函數(shù)迄汛,這樣的話 kotlin 就可以使用以前說的閉包的寫法:() 里面寫常規(guī)參數(shù)捍壤,{} 里面寫函數(shù)式對象的實(shí)現(xiàn),就像上面的例子一樣
我們需要關(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 線程池
CoroutineStart - 啟動模式疗隶,默認(rèn)是DEAFAULT,也就是創(chuàng)建就啟動翼闹;還有一個(gè)是LAZY斑鼻,意思是等你需要它的時(shí)候,再調(diào)用啟動
DEAFAULT - 模式模式橄碾,不寫就是默認(rèn)
ATOMIC -
UNDISPATCHED
LAZY - 懶加載模式卵沉,你需要它的時(shí)候颠锉,再調(diào)用啟動
block - 閉包方法體,定義協(xié)程內(nèi)需要執(zhí)行的操作
Job - 協(xié)程構(gòu)建函數(shù)的返回值史汗,可以把 Job 看成協(xié)程對象本身琼掠,協(xié)程的操作方法都在 Job 身上了
job.start() - 啟動協(xié)程,除了 lazy 模式停撞,協(xié)程都不需要手動啟動
job.join() - 等待協(xié)程執(zhí)行完畢
job.cancel() - 取消一個(gè)協(xié)程
job.cancelAndJoin() - 等待協(xié)程執(zhí)行完畢然后再取消
創(chuàng)建一個(gè)協(xié)程
創(chuàng)建該launch函數(shù)返回了一個(gè)可以被用來取消運(yùn)行中的協(xié)程的Job
val job=launch{
? ? repeat(1000){i->
? ? ? ? println("job: I'm sleeping$i...")
? ? ? ? delay(500L)
? ? }
}
取消
val? job=launch{
repeat(1000){
i->println("job: I'm sleeping $i ...")
delay(500L)}}
delay(1300L)// 延遲一段時(shí)間
println("main: I'm tired of waiting!")job.cancel()// 取消該作業(yè)
job.join()// 等待作業(yè)執(zhí)行結(jié)束
println("main: Now I can quit.")
程序執(zhí)行后的輸出如下:
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.
一旦 main 函數(shù)調(diào)用了 job.cancel瓷蛙,我們在其它的協(xié)程中就看不到任何輸出,因?yàn)樗蝗∠?/p>
超時(shí)
在實(shí)踐中絕大多數(shù)取消一個(gè)協(xié)程的理由是它有可能超時(shí)戈毒。 當(dāng)你手動追蹤一個(gè)相關(guān) Job的引用并啟動了一個(gè)單獨(dú)的協(xié)程在延遲后取消追蹤艰猬,這里已經(jīng)準(zhǔn)備好使用 withTimeout 函數(shù)來做這件事。 來看看示例代碼:
withTimeout(1300L){repeat(1000){i->
println("I'm sleeping $i ...")
delay(500L)}}
運(yùn)行后得到如下輸出:
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms
這里我們看到了TimeoutCancellationException異常,這異常是因?yàn)槌瑫r(shí)導(dǎo)致的異常取消
解決這個(gè)問題也很簡單通過withTimeout的withTimeoutOrNull函數(shù),代碼示例:
valresult=withTimeoutOrNull(1300L){repeat(1000){i->println("I'm sleeping$i...")delay(500L)}"Done"http:// 在它運(yùn)行得到結(jié)果之前取消它}println("Result is$result")
運(yùn)行后得到如下輸出:
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Result is null
這樣就沒有拋出異常了
async 函數(shù)定義
public fun <T> CoroutineScope.async(
? ? context:CoroutineContext=EmptyCoroutineContext,
? ? start:CoroutineStart=CoroutineStart.DEFAULT,
? ? block:suspendCoroutineScope.()->T):Deferred<T>{
? ? ? ? val? newContext=newCoroutineContext(context)
? ? ? ? val coroutine=if(start.isLazy){
? ? ? ? LazyDeferredCoroutine(newContext,block)
}else{DeferredCoroutine<T>(newContext,active=true)
? ? ? ? coroutine.start(start,coroutine,block)
? ? ? ? return coroutine
}
從源碼可以看出launch 和 async的唯一區(qū)別在于async的返回值
async 返回的是 Deferred 類型埋市,Deferred 繼承自 Job 接口冠桃,Job有的它都有,增加了一個(gè)方法 await 道宅,這個(gè)方法接收的是 async 閉包中返回的值食听,async 的特點(diǎn)是不會阻塞當(dāng)前線程,但會阻塞所在協(xié)程污茵,也就是掛起
但是需要注意的是async 并不會阻塞線程樱报,只是阻塞鎖調(diào)用的協(xié)程
async和launch的區(qū)別
launch 更多是用來發(fā)起一個(gè)無需結(jié)果的耗時(shí)任務(wù),這個(gè)工作不需要返回結(jié)果泞当。
async 函數(shù)則是更進(jìn)一步迹蛤,用于異步執(zhí)行耗時(shí)任務(wù),并且需要返回值(如網(wǎng)絡(luò)請求襟士、數(shù)據(jù)庫讀寫盗飒、文件讀寫),在執(zhí)行完畢通過 await() 函數(shù)獲取返回值敌蜂。
runBlocking
runBlocking啟動的協(xié)程任務(wù)會阻斷當(dāng)前線程箩兽,直到該協(xié)程執(zhí)行結(jié)束。當(dāng)協(xié)程執(zhí)行結(jié)束之后章喉,頁面才會被顯示出來。
runBlocking 通常適用于單元測試的場景身坐,而業(yè)務(wù)開發(fā)中不會用到這個(gè)函數(shù)
relay秸脱、yield
relay 和 yield 方法是協(xié)程內(nèi)部的操作,可以掛起協(xié)程部蛇,
relay摊唇、yield 的區(qū)別
relay 是掛起協(xié)程并經(jīng)過執(zhí)行時(shí)間恢復(fù)協(xié)程,當(dāng)線程空閑時(shí)就會運(yùn)行協(xié)程
yield 是掛起協(xié)程涯鲁,讓協(xié)程放棄本次 cpu 執(zhí)行機(jī)會讓給別的協(xié)程巷查,當(dāng)線程空閑時(shí)再次運(yùn)行協(xié)程有序。
我們只要使用 kotlin 提供的協(xié)程上下文類型,線程池是有多個(gè)線程的岛请,再次執(zhí)行的機(jī)會很快就會有的旭寿。
除了 main 類型,協(xié)程在掛起后都會封裝成任務(wù)放到協(xié)程默認(rèn)線程池的任務(wù)隊(duì)列里去崇败,有延遲時(shí)間的在時(shí)間過后會放到隊(duì)列里去盅称,沒有延遲時(shí)間的直接放到隊(duì)列里去
原文鏈接:http://www.reibang.com/p/402a69dbd66d