協(xié)程-基礎(chǔ)

第一個協(xié)程程序

添加依賴
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2'

fun main() {

    // 在后臺啟動一個新的協(xié)程并繼續(xù)
    GlobalScope.launch { 
        delay(1000L) // 非阻塞的等待 1 秒鐘(默認(rèn)時間單位是毫秒)  阻塞子線程
        println("World!") // 在延遲后打印輸出
    }
    
    println("Hello,") // 協(xié)程已在等待時主線程還在繼續(xù)
    Thread.sleep(2000L) // 阻塞主線程 2 秒鐘來保證 JVM 存活
    
}

輸出結(jié)果
Hello,
World!

本質(zhì)上藕漱,協(xié)程是輕量級的線程窥妇。 它們在某些 CoroutineScope 上下文中與 launch 協(xié)程構(gòu)建器 一起啟動疯汁。 這里我們在 GlobalScope 中啟動了一個新的協(xié)程,這意味著新協(xié)程的生命周期只受整個應(yīng)用程序的生命周期限制宪卿。

可以將 GlobalScope.launch { …… } 替換為 thread { …… },并將 delay(……) 替換為 Thread.sleep(……) 達(dá)到同樣目的万栅。 試試看(不要忘記導(dǎo)入 kotlin.concurrent.thread)愧捕。

fun main() {

    thread {
        println("World!")
        Thread.sleep(1000L)
    }
    println("Hello,")
    Thread.sleep(2000L)

}

如果你首先將 GlobalScope.launch 替換為 thread,編譯器會報以下錯誤:

Error: Kotlin: Suspend functions are only allowed to be called from a coroutine or another suspend function

這是因為 delay 是一個特殊的 掛起函數(shù) 申钩,它不會造成線程阻塞次绘,但是會 掛起 協(xié)程,并且只能在協(xié)程中使用撒遣。

橋接阻塞與非阻塞的世界

第一個示例在同一段代碼中混用了 非阻塞的 delay(……)阻塞的 Thread.sleep(……)邮偎。 這容易讓我們記混哪個是阻塞的、哪個是非阻塞的义黎。 讓我們顯式使用 runBlocking 協(xié)程構(gòu)建器來阻塞:

fun main() {
    GlobalScope.launch { // 在后臺啟動一個新的協(xié)程并繼續(xù)
        delay(1000L)
        println("World!")
    }
    println("Hello,") // 主線程中的代碼會立即執(zhí)行
    runBlocking {     // 但是這個表達(dá)式阻塞了主線程
        delay(2000L)  // ……我們延遲 2 秒來保證 JVM 的存活
    }
}

結(jié)果是相似的禾进,但是這些代碼只使用了非阻塞的函數(shù) delay。 調(diào)用了 runBlocking 的主線程會一直 阻塞 直到 runBlocking 內(nèi)部的協(xié)程執(zhí)行完畢廉涕。

等待一個作業(yè)

延遲一段時間來等待另一個協(xié)程運行并不是一個好的選擇泻云。讓我們顯式(以非阻塞方式)等待所啟動的后臺 Job 執(zhí)行結(jié)束:

val job = GlobalScope.launch { // 啟動一個新協(xié)程并保持對這個作業(yè)的引用
    delay(1000L)
    println("World!")
}
println("Hello,")
job.join() //等待直到子協(xié)程執(zhí)行結(jié)束

現(xiàn)在,結(jié)果仍然相同狐蜕,但是主協(xié)程與后臺作業(yè)的持續(xù)時間沒有任何關(guān)系了宠纯。好多了。

結(jié)構(gòu)化的并發(fā)

協(xié)程的實際使用還有一些需要改進(jìn)的地方层释。 當(dāng)我們使用 GlobalScope.launch 時婆瓜,我們會創(chuàng)建一個頂層協(xié)程。雖然它很輕量贡羔,但它運行時仍會消耗一些內(nèi)存資源廉白。如果我們忘記保持對新啟動的協(xié)程的引用,它還會繼續(xù)運行乖寒。如果協(xié)程中的代碼掛起了會怎么樣(例如猴蹂,我們錯誤地延遲了太長時間),如果我們啟動了太多的協(xié)程并導(dǎo)致內(nèi)存不足會怎么樣楣嘁? 必須手動保持對所有已啟動協(xié)程的引用并 join 之很容易出錯磅轻。

有一個更好的解決辦法覆获。我們可以在代碼中使用結(jié)構(gòu)化并發(fā)。 我們可以在執(zhí)行操作所在的指定作用域內(nèi)啟動協(xié)程瓢省, 而不是像通常使用線程(線程總是全局的)那樣在 GlobalScope 中啟動弄息。

在我們的示例中,我們使用 runBlocking 協(xié)程構(gòu)建器將 main 函數(shù)轉(zhuǎn)換為協(xié)程勤婚。 包括 runBlocking 在內(nèi)的每個協(xié)程構(gòu)建器都將 CoroutineScope 的實例添加到其代碼塊所在的作用域中摹量。 我們可以在這個作用域中啟動協(xié)程而無需顯式 join 之,因為外部協(xié)程(示例中的 runBlocking)直到在其作用域中啟動的所有協(xié)程都執(zhí)行完畢后才會結(jié)束馒胆。因此缨称,可以將我們的示例簡化為:

import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
    launch { // 在 runBlocking 作用域中啟動一個新協(xié)程
        delay(1000L)
        println("World!")
    }
    println("Hello,")
}

作用域構(gòu)建器

除了由不同的構(gòu)建器提供協(xié)程作用域之外,還可以使用 coroutineScope 構(gòu)建器聲明自己的作用域祝迂。它會創(chuàng)建一個協(xié)程作用域并且在所有已啟動子協(xié)程執(zhí)行完畢之前不會結(jié)束睦尽。

runBlockingcoroutineScope 可能看起來很類似,因為它們都會等待其協(xié)程體以及所有子協(xié)程結(jié)束型雳。 主要區(qū)別在于当凡,runBlocking 方法會阻塞當(dāng)前線程來等待, 而 coroutineScope 只是掛起纠俭,會釋放底層線程用于其他用途沿量。 由于存在這點差異,runBlocking 是常規(guī)函數(shù)冤荆,而 coroutineScope 是掛起函數(shù)朴则。

可以通過以下示例來演示:

fun main() = runBlocking {
    launch {
        delay(200L)
        println("Task from runBlocking")
    }

    coroutineScope { // 創(chuàng)建一個協(xié)程作用域
        launch {
            delay(500L)
            println("Task from nested launch")
        }

        delay(100L)
        println("Task from coroutine scope") // 這一行會在內(nèi)嵌 launch 之前輸出
    }

    println("Coroutine scope is over") // 這一行在內(nèi)嵌 launch 執(zhí)行完畢后才輸出
}

輸出結(jié)果
Task from coroutine scope
Task from runBlocking
Task from nested launch
Coroutine scope is over

請注意,(當(dāng)?shù)却齼?nèi)嵌 launch 時)緊挨“Task from coroutine scope”消息之后钓简, 就會執(zhí)行并輸出“Task from runBlocking”——盡管 coroutineScope 尚未結(jié)束乌妒。

提取函數(shù)重構(gòu)

我們來將 launch { …… } 內(nèi)部的代碼塊提取到獨立的函數(shù)中。當(dāng)你對這段代碼執(zhí)行“提取函數(shù)”重構(gòu)時外邓,你會得到一個帶有 suspend 修飾符的新函數(shù)撤蚊。 這是你的第一個掛起函數(shù)。在協(xié)程內(nèi)部可以像普通函數(shù)一樣使用掛起函數(shù)坐榆, 不過其額外特性是拴魄,同樣可以使用其他掛起函數(shù)(如本例中的 delay)來掛起協(xié)程的執(zhí)行。

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch { doWorld() }
    println("Hello,")
}

// 這是你的第一個掛起函數(shù)
suspend fun doWorld() {
    delay(1000L)
    println("World!")
}

但是如果提取出的函數(shù)包含一個在當(dāng)前作用域中調(diào)用的協(xié)程構(gòu)建器的話席镀,該怎么辦? 在這種情況下夏漱,所提取函數(shù)上只有 suspend 修飾符是不夠的豪诲。為 CoroutineScope 寫一個 doWorld 擴展方法是其中一種解決方案,但這可能并非總是適用挂绰,因為它并沒有使 API 更加清晰屎篱。 慣用的解決方案是要么顯式將 CoroutineScope 作為包含該函數(shù)的類的一個字段服赎, 要么當(dāng)外部類實現(xiàn)了 CoroutineScope 時隱式取得。 作為最后的手段交播,可以使用 CoroutineScope(coroutineContext)重虑,不過這種方法結(jié)構(gòu)上不安全, 因為你不能再控制該方法執(zhí)行的作用域秦士。只有私有 API 才能使用這個構(gòu)建器缺厉。

協(xié)程很輕量

import kotlinx.coroutines.*

fun main() = runBlocking {
    repeat(100_000) { // 啟動大量的協(xié)程
        launch {
            delay(5000L)
            print(".")
        }
    }
}

它啟動了 10 萬個協(xié)程,并且在 5 秒鐘后隧土,每個協(xié)程都輸出一個點提针。
現(xiàn)在,嘗試使用線程來實現(xiàn)曹傀。會發(fā)生什么辐脖?(很可能你的代碼會產(chǎn)生某種內(nèi)存不足的錯誤)

全局協(xié)程像守護線程

以下代碼在 GlobalScope 中啟動了一個長期運行的協(xié)程,該協(xié)程每秒輸出“I'm sleeping”兩次皆愉,之后在主函數(shù)中延遲一段時間后返回嗜价。

GlobalScope.launch {
    repeat(1000) { i ->
        println("I'm sleeping $i ...")
        delay(500L)
    }
}
delay(1300L) // 在延遲后退出

你可以運行這個程序并看到它輸出了以下三行后終止:
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...

GlobalScope 中啟動的活動協(xié)程并不會使進(jìn)程保活幕庐。它們就像守護線程炭剪。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市翔脱,隨后出現(xiàn)的幾起案子奴拦,更是在濱河造成了極大的恐慌,老刑警劉巖届吁,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件错妖,死亡現(xiàn)場離奇詭異,居然都是意外死亡疚沐,警方通過查閱死者的電腦和手機暂氯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來亮蛔,“玉大人痴施,你說我怎么就攤上這事【苛鳎” “怎么了辣吃?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長芬探。 經(jīng)常有香客問我神得,道長,這世上最難降的妖魔是什么偷仿? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任哩簿,我火速辦了婚禮宵蕉,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘节榜。我一直安慰自己羡玛,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布宗苍。 她就那樣靜靜地躺著稼稿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪浓若。 梳的紋絲不亂的頭發(fā)上渺杉,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音挪钓,去河邊找鬼是越。 笑死,一個胖子當(dāng)著我的面吹牛碌上,可吹牛的內(nèi)容都是我干的倚评。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼馏予,長吁一口氣:“原來是場噩夢啊……” “哼天梧!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起霞丧,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤呢岗,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蛹尝,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體后豫,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年突那,在試婚紗的時候發(fā)現(xiàn)自己被綠了挫酿。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡愕难,死狀恐怖早龟,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情猫缭,我是刑警寧澤葱弟,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站饵骨,受9級特大地震影響翘悉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜居触,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一妖混、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧轮洋,春花似錦制市、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至汉柒,卻和暖如春误褪,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背碾褂。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工兽间, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人正塌。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓嘀略,卻偏偏與公主長得像,于是被迫代替她去往敵國和親乓诽。 傳聞我的和親對象是個殘疾皇子帜羊,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

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