第一個協(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é)束睦尽。
runBlocking 與 coroutineScope 可能看起來很類似,因為它們都會等待其協(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)程保活幕庐。它們就像守護線程炭剪。