目錄
kotlin之協(xié)程(一),線程,進(jìn)程,協(xié)程,協(xié)程可以替換線程嗎?
kotlin之協(xié)程(二),Kotlin協(xié)程是什么唾糯、掛起是什么怠硼、掛起的非阻塞式
kotlin之協(xié)程(三),開始創(chuàng)建協(xié)程,launch,withContext
kotlin之協(xié)程(四),協(xié)程的核心關(guān)鍵字suspend
kotlin之協(xié)程(五),launch 函數(shù)以及協(xié)程的取消與超時(shí)
kotlin之協(xié)程(六),協(xié)程中的 async和launch的區(qū)別以及runBlocking
kotlin之協(xié)程(七),協(xié)程中relay、yield 區(qū)別
前言
這里推薦一下"扔物線"的的網(wǎng)站,看了他講解讓我對(duì)kotlin的協(xié)程有了更深刻的了解,而我接下來的文章來自"扔物線"的講述協(xié)程
傳送地址: 扔物線-協(xié)程
扔物線
碼上開學(xué)創(chuàng)始人移怯、項(xiàng)目管理人香璃、內(nèi)容模塊規(guī)劃者和視頻內(nèi)容作者。
Android GDE( Google 認(rèn)證 Android 開發(fā)專家)舟误,前 Flipboard Android 工程師葡秒。
GitHub 全球 Java 排名第 92 位,在 GitHub 上有 6.6k followers 和 9.9k stars。
個(gè)人的 Android 開源庫 MaterialEditText 被全球多個(gè)項(xiàng)目引用同云,其中包括在全球擁有 5 億用戶的新聞閱讀軟件 Flipboard 糖权。
曾多次在 Google Developer Group Beijing 線下分享會(huì)中擔(dān)任 Android 部分的講師。
關(guān)于kotlin中的協(xié)程概念
首先上篇文章中說到的協(xié)程并不是 Kotlin 提出來的新概念,很多語言都有這個(gè)特性
而網(wǎng)上對(duì)協(xié)程的概念有很多描述,上片文章里面就收集了很多對(duì)于協(xié)程的描述,但是協(xié)程本身沒有一個(gè)明確的定義
拋開其他語言,對(duì)于kotlin而言
協(xié)程就是 Kotlin 提供的一套
線程封裝的 API
(線程框架)炸站,但并不是說協(xié)程就是為線程而生的星澳。kotlin中的協(xié)程類似于 Java 自帶的 Executor 系列 API 或者 Android 的 Handler 系列 API。
不過旱易,我們學(xué)習(xí) Kotlin 中的協(xié)程禁偎,一開始確實(shí)可以從線程控制的角度來切入。因?yàn)樵?Kotlin 中阀坏,協(xié)程的一個(gè)典型的使用場景就是線程控制如暖。就像 Java 中的 Executor 和 Android 中的 AsyncTask,Kotlin 中的協(xié)程也有對(duì) Thread API 的封裝忌堂,讓我們可以在寫代碼時(shí)盒至,不用關(guān)注多線程就能夠很方便地寫出并發(fā)操作。
從 Android 開發(fā)者的角度去理解協(xié)程和線程的關(guān)系:
我們所有的代碼都是跑在線程中的士修,而線程是跑在進(jìn)程中的枷遂。
協(xié)程沒有直接和操作系統(tǒng)關(guān)聯(lián),但它不是空中樓閣棋嘲,它也是跑在線程中的酒唉,可以是單線程,也可以是多線程沸移。
單線程中的協(xié)程總的執(zhí)行時(shí)間并不會(huì)比不用協(xié)程少痪伦。
kotlin中使用協(xié)程進(jìn)行耗時(shí)操作的本質(zhì)是什么
Android 系統(tǒng)上,如果在主線程進(jìn)行網(wǎng)絡(luò)請(qǐng)求雹锣,會(huì)拋出 NetworkOnMainThreadException网沾,對(duì)于在主線程上的協(xié)程也不例外,這種場景使用協(xié)程還是要切線程的蕊爵。
kotlin中協(xié)程的特點(diǎn)
而對(duì)于kotlin中協(xié)程最重要的一點(diǎn)特點(diǎn)是,是可以用同步的方式寫出異步的代碼
下面是kotlin中協(xié)程一種寫法:
coroutineScope.launch(Dispatchers.Main) { // 在主線程開啟協(xié)程
val user = api.getUser() // IO 線程執(zhí)行網(wǎng)絡(luò)請(qǐng)求
nameTv.text = user.name // 主線程更新 UI
}
而通過 Java 實(shí)現(xiàn)以上邏輯辉哥,我們通常需要這樣寫:
api.getUser(new Callback<User>() {
@Override
public void success(User user) {
runOnUiThread(new Runnable() {
@Override
public void run() {
nameTv.setText(user.name);
}
})
}
@Override
public void failure(Exception e) {
...
}
});
Java的這種寫法回調(diào)打亂了代碼的順序性,使得閱讀非常的難受
對(duì)于回調(diào)式的寫法,如果并發(fā)場景再復(fù)雜一些在辆,代碼的嵌套可能會(huì)更多证薇,這樣的話維護(hù)起來就非常麻煩。但如果你使用了 Kotlin 協(xié)程匆篓,多層網(wǎng)絡(luò)請(qǐng)求只需要這么寫:
coroutineScope.launch(Dispatchers.Main) { // 開始協(xié)程:主線程
val token = api.getToken() // 網(wǎng)絡(luò)請(qǐng)求:IO 線程
val user = api.getUser(token) // 網(wǎng)絡(luò)請(qǐng)求:IO 線程
nameTv.text = user.name // 更新 UI:主線程
}
可以看到浑度,即便是比較復(fù)雜的并行網(wǎng)絡(luò)請(qǐng)求,也能夠通過協(xié)程寫出結(jié)構(gòu)清晰的代碼
協(xié)程中掛起的本質(zhì)
協(xié)程中「掛起」的對(duì)象到底是什么鸦概?掛起線程箩张,還是掛起函數(shù)甩骏?都不對(duì),我們掛起的對(duì)象是協(xié)程
啟動(dòng)一個(gè)協(xié)程可以使用 launch 或者 async 函數(shù)先慷,協(xié)程其實(shí)就是這兩個(gè)函數(shù)中閉包的代碼塊饮笛。
launch ,async 或者其他函數(shù)創(chuàng)建的協(xié)程论熙,在執(zhí)行到某一個(gè) suspend 函數(shù)的時(shí)候福青,這個(gè)協(xié)程會(huì)被「suspend」,也就是被掛起脓诡。
注意:關(guān)于launch async suspend 這些概念我們后續(xù)會(huì)講大家先知道這是協(xié)程的一個(gè)內(nèi)容
那此時(shí)又是從哪里掛起无午?從當(dāng)前線程掛起。換句話說祝谚,就是這個(gè)協(xié)程從正在執(zhí)行它的線程上脫離宪迟。
suspend 是有暫停的意思,但我們?cè)趨f(xié)程中應(yīng)該理解為:當(dāng)線程執(zhí)行到協(xié)程的 suspend 函數(shù)的時(shí)候交惯,暫時(shí)不繼續(xù)執(zhí)行協(xié)程代碼了次泽。
我們先讓時(shí)間靜止,然后兵分兩路席爽,分別看看這兩個(gè)互相脫離的線程和協(xié)程接下來將會(huì)發(fā)生什么事情:
線程:
前面我們提到意荤,掛起會(huì)讓協(xié)程從正在執(zhí)行它的線程上脫離,具體到代碼其實(shí)是:
協(xié)程的代碼塊中拳昌,線程執(zhí)行到了 suspend 函數(shù)這里的時(shí)候袭异,就暫時(shí)不再執(zhí)行剩余的協(xié)程代碼钠龙,跳出協(xié)程的代碼塊七兜。
那線程接下來會(huì)做什么呢宜猜?
如果它是一個(gè)后臺(tái)線程:
要么無事可做,被系統(tǒng)回收
要么繼續(xù)執(zhí)行別的后臺(tái)任務(wù)
跟 Java 線程池里的線程在工作結(jié)束之后是完全一樣的:回收或者再利用。
如果這個(gè)線程它是 Android 的主線程茶没,那它接下來就會(huì)繼續(xù)回去工作:也就是一秒鐘 60 次的界面刷新任務(wù)。
一個(gè)常見的場景是仓手,獲取一個(gè)圖片向瓷,然后顯示出來:
// 主線程中
GlobalScope.launch(Dispatchers.Main) {
val image = suspendingGetImage(imageId) // 獲取圖片
avatarIv.setImageBitmap(image) // 顯示出來
}
suspend fun suspendingGetImage(id: String) = withContext(Dispatchers.IO) {
...
}
這段執(zhí)行在主線程的協(xié)程,它實(shí)質(zhì)上會(huì)往你的主線程 post 一個(gè) Runnable根竿,這個(gè) Runnable 就是你的協(xié)程代碼:
handler.post {
val image = suspendingGetImage(imageId)
avatarIv.setImageBitmap(image)
}
當(dāng)這個(gè)協(xié)程被掛起的時(shí)候陵像,就是主線程 post
的這個(gè) Runnable
提前結(jié)束,然后繼續(xù)執(zhí)行它界面刷新的任務(wù)寇壳。
關(guān)于線程醒颖,我們就看完了。
這個(gè)時(shí)候你可能會(huì)有一個(gè)疑問壳炎,那 launch 包裹的剩下代碼怎么辦泞歉?
所以接下來,我們來看看協(xié)程這一邊。
協(xié)程:
線程的代碼在到達(dá) suspend
函數(shù)的時(shí)候被掐斷腰耙,接下來協(xié)程會(huì)從這個(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
:針對(duì)磁盤和網(wǎng)絡(luò) IO 進(jìn)行了優(yōu)化,適合 IO 密集型的任務(wù)寻歧,比如:讀寫文件掌栅,操作數(shù)據(jù)庫以及網(wǎng)絡(luò)請(qǐng)求 -
Dispatchers.Default
:適合 CPU 密集型的任務(wù),比如計(jì)算
回到我們的協(xié)程码泛,它從 suspend
函數(shù)開始脫離啟動(dòng)它的線程猾封,繼續(xù)執(zhí)行在 Dispatchers
所指定的 IO 線程。
緊接著在 suspend
函數(shù)執(zhí)行完成之后噪珊,協(xié)程為我們做的最爽的事就來了:會(huì)自動(dòng)幫我們把線程再切回來晌缘。
這個(gè)「切回來」是什么意思?
我們的協(xié)程原本是運(yùn)行在主線程的痢站,當(dāng)代碼遇到 suspend 函數(shù)的時(shí)候磷箕,發(fā)生線程切換,根據(jù) Dispatchers
切換到了 IO 線程阵难;
當(dāng)這個(gè)函數(shù)執(zhí)行完畢后岳枷,線程又切了回來,「切回來」也就是協(xié)程會(huì)幫我再 post
一個(gè) Runnable
呜叫,讓我剩下的代碼繼續(xù)回到主線程去執(zhí)行空繁。
我們從線程和協(xié)程的兩個(gè)角度都分析完成后,終于可以對(duì)協(xié)程的「掛起」suspend 做一個(gè)解釋:
協(xié)程在執(zhí)行到有 suspend 標(biāo)記的函數(shù)的時(shí)候朱庆,會(huì)被 suspend 也就是被掛起盛泡,而所謂的被掛起,就是切個(gè)線程椎工;
不過區(qū)別在于饭于,掛起函數(shù)在執(zhí)行完成之后蜀踏,協(xié)程會(huì)重新切回它原先的線程。
再簡單來講掰吕,在 Kotlin 中所謂的掛起果覆,就是一個(gè)稍后會(huì)被自動(dòng)切回來的線程調(diào)度操作。
這個(gè)「切回來」的動(dòng)作殖熟,在 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í)直接或者間接地饼齿,總是會(huì)在一個(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í)直接或者間接地运翼,總是會(huì)在一個(gè)協(xié)程里被調(diào)用的返干。
所以兴枯,要求 suspend
函數(shù)只能在協(xié)程里或者另一個(gè) suspend 函數(shù)里被調(diào)用血淌,還是為了要讓協(xié)程能夠在 suspend 函數(shù)切換線程之后再切回來。
什么是「非阻塞式掛起」
非阻塞式是相對(duì)阻塞式而言的财剖。
編程語言中的很多概念其實(shí)都來源于生活悠夯,就像脫口秀的段子一樣。
線程阻塞很好理解躺坟,現(xiàn)實(shí)中的例子就是交通堵塞沦补,它的核心有 3 點(diǎn):
- 前面有障礙物,你過不去(線程卡了)
- 需要等障礙物清除后才能過去(耗時(shí)任務(wù)結(jié)束)
- 除非你繞道而行(切到別的線程)
從語義上理解「非阻塞式掛起」咪橙,講的是「非阻塞式」這個(gè)是掛起的一個(gè)特點(diǎn)夕膀,也就是說虚倒,協(xié)程的掛起,就是非阻塞式的产舞,協(xié)程是不講「阻塞式的掛起」的概念的魂奥。
我們講「非阻塞式掛起」,其實(shí)它有幾個(gè)前提:并沒有限定在一個(gè)線程里說這件事易猫,因?yàn)閽炱疬@件事耻煤,本來就是涉及到多線程。
就像視頻里講的准颓,阻塞不阻塞哈蝇,都是針對(duì)單線程講的,一旦切了線程攘已,肯定是非阻塞的炮赦,你都跑到別的線程了,之前的線程就自由了样勃,可以繼續(xù)做別的事情了眼五。
所以「非阻塞式掛起」,其實(shí)就是在講協(xié)程在掛起的同時(shí)切線程這件事情彤灶。
為什么要講非阻塞式掛起
「非阻塞式掛起」和第二篇的「掛起要切線程」是同一件事情看幼,那還有講的必要嗎?
是有的幌陕。因?yàn)樗趯懛ㄉ虾蛦尉€程的阻塞式是一樣的诵姜。
協(xié)程只是在寫法上「看起來阻塞」,其實(shí)是「非阻塞」的搏熄,因?yàn)樵趨f(xié)程里面它做了很多工作棚唆,其中有一個(gè)就是幫我們切線程。
之前說的掛起心例,重點(diǎn)是說切線程先切過去宵凌,然后再切回來。
而這里的非阻塞式止后,重點(diǎn)是說線程雖然會(huì)切瞎惫,但寫法上和普通的單線程差不多。
讓我們來看看下面的例子:
main {
GlobalScope.launch(Dispatchers.Main) {
// 耗時(shí)操作
val user = suspendingRequestUser()
updateView(user)
}
private suspend fun suspendingRequestUser() : User = withContext(Dispatchers.IO) {
api.requestUser()
}
}
從上面的例子可以看到译株,耗時(shí)操作和更新 UI 的邏輯像寫單線程一樣放在了一起瓜喇,只是在外面包了一層協(xié)程。
而正是這個(gè)協(xié)程解決了原來我們單線程寫法會(huì)卡線程這件事歉糜。
阻塞的本質(zhì)
首先乘寒,所有的代碼本質(zhì)上都是阻塞式的,而只有比較耗時(shí)的代碼才會(huì)導(dǎo)致人類可感知的等待匪补,比如在主線程上做一個(gè)耗時(shí) 50 ms 的操作會(huì)導(dǎo)致界面卡掉幾幀伞辛,這種是我們?nèi)搜勰苡^察出來的烂翰,而這就是我們通常意義所說的「阻塞」。
舉個(gè)例子蚤氏,當(dāng)你開發(fā)的 app 在性能好的手機(jī)上很流暢刽酱,在性能差的老手機(jī)上會(huì)卡頓,就是在說同一行代碼執(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é)程就是切線程柱告;
- 掛起是什么
掛起就是可以自動(dòng)切回來的切線程;
- 掛起的非阻塞式
掛起的非阻塞式指的是它能用看起來阻塞的代碼寫出非阻塞的操作笑陈。