序言
如果對協(xié)程沒有概念闸婴,不了解使用協(xié)程的好處您宪,請參考《異步編程
》系列文章
引入?yún)f(xié)程庫
kotlin協(xié)程是以一個lib包的形式引入的奈懒,參考: kotlinx.coroutines
這里摘錄gradle方式的協(xié)程庫引入
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
}
請確認(rèn)使用了最新版本的kotlin
buildscript {
ext.kotlin_version = '1.3.61'
}
語法講解
此部分翻譯至 https://kotlinlang.org/docs/reference/coroutines/basics.html,作了微調(diào)
第一個協(xié)程程序
我們使用協(xié)程來寫一個helloworld程序
fun main() {
GlobalScope.launch { // 啟動并后臺運行一個協(xié)程
delay(1000L) // 非阻塞的暫停1s
println("World!")
}
println("Hello,") // 主線程繼續(xù)執(zhí)行
Thread.sleep(2000L) // 主線程睡眠2秒鐘宪巨,保持當(dāng)前jvm進(jìn)程存活
}
輸出:
Hello,
World!
本質(zhì)上磷杏,協(xié)程是一種輕量級的線程。通過launch
關(guān)鍵字啟動捏卓,launch
是一個協(xié)程構(gòu)建器
极祸,攜帶一種CoroutineScope的上下文。上面的代碼中怠晴,我們在Global Scope中啟動了協(xié)程遥金,這意味著,被啟動的協(xié)程的生命周期與整個進(jìn)程保持一致龄寞。
我們可以通過把GlobalScope.launch { ... }
替換為thread { ... }
并把delay(...)
替換為Thread.sleep(...)
來實現(xiàn)同樣的效果(去試試吧L妗)
如果你把 GlobalScope.launch 替換為 thread(譯者:還沒來得及替換delay), 編譯器會報如下的錯:
Error: Kotlin: Suspend functions are only allowed to be called from a coroutine or another suspend function
這是因為delay是一個suspending function
并不會阻塞線程但是會阻塞協(xié)程汤功,而且他只能在協(xié)程中被調(diào)用物邑。
阻塞和非阻塞世界的橋梁
上面的例子在同一段代碼中混用了 non-blocking delay(...)
和 blocking Thread.sleep(...)
. 這樣很容易讓我們混淆哪些是blocking哪些不是blocking的代碼. 讓我們通過 runBlocking 協(xié)程構(gòu)建器來提出阻塞的代碼吧:
fun main() {
GlobalScope.launch { // 啟動并在后臺運行協(xié)程
delay(1000L)
println("World!")
}
println("Hello,") // 主線程直接繼續(xù)執(zhí)行
runBlocking { // 會阻塞主線程的執(zhí)行
delay(2000L) // 保持jvm的持續(xù)運行
}
}
上面的代碼執(zhí)行結(jié)果是一樣的,但是只用了非阻塞的delay
滔金。主線程執(zhí)行了runBlocking
代碼塊色解,并且主線程在其代碼塊內(nèi)部執(zhí)行完成之前會被阻塞。
上面例子里的代碼可以被重寫成更加通用的方式:
fun main() = runBlocking<Unit> { // 啟動主協(xié)程
GlobalScope.launch { // 啟動新協(xié)程并在后臺允許
delay(1000L)
println("World!")
}
println("Hello,") // 主協(xié)程此處直接繼續(xù)執(zhí)行
delay(2000L) // 睡兩秒保持jvm運行
}
這里使用了runBlocking<Unit> { ... }
作為一個適配器餐茵,用來在最頂層啟動主協(xié)程科阎,我們聲明了Unit作為他的返回值類型,因為main函數(shù)需要如此忿族。
這也是對suspending
函數(shù)做單元測試的一種方法:
class MyTest {
@Test
fun testMySuspendingFunction() = runBlocking<Unit> {
// here we can use suspending functions using any assertion style that we like
}
}
等待一個任務(wù)的執(zhí)行
上面幾個例子里锣笨,都是通過主函數(shù)最后睡幾秒來保持jvm進(jìn)程的運行狀態(tài),這并不是很好道批,讓我們來更精確的控制時間:
val job = GlobalScope.launch { //此處獲得一個job的引用
delay(1000L)
println("World!")
}
println("Hello,")
job.join() // 主協(xié)程等待错英,直到j(luò)ob執(zhí)行完畢
現(xiàn)在,執(zhí)行結(jié)果依然相同隆豹,但代碼好看多了椭岩!
結(jié)構(gòu)化并發(fā)
如果想要在實際中使用協(xié)程,還需要考慮一些其他的事情。當(dāng)我們使用GlobalScope.launch
時判哥,我們就創(chuàng)建了top-level的協(xié)程献雅。雖然協(xié)程是輕量的,但協(xié)程運行時畢竟還是要消耗一些內(nèi)存的塌计。如果我們對新創(chuàng)建的協(xié)程沒有保持引用挺身,而協(xié)程恰巧hangs住了,如果我們跑了海量的協(xié)程并發(fā)生內(nèi)存溢出...必須要手動管理已啟動協(xié)程的引用锌仅,join 這些協(xié)程是容易出錯的瞒渠。
有更好的解決方案,我們可以在代碼里使用結(jié)構(gòu)化并發(fā):基于當(dāng)前操作的scope創(chuàng)建所需的協(xié)程技扼,而不是使用GlobalScope伍玖。
在我們的例子里我們的main函數(shù)通過runBlocking協(xié)程構(gòu)建器轉(zhuǎn)換成一個協(xié)程函數(shù)。包括runBlocking
在內(nèi)的每一個協(xié)程構(gòu)造器都會給它對應(yīng)的代碼塊附加一個CoroutineScope實例剿吻。
我們可以基于這個scope創(chuàng)建新的協(xié)程窍箍,而不需顯示的
join
,因為外層的協(xié)程在這個scope內(nèi)的所有協(xié)程介紹之前不會結(jié)束丽旅。所以我們可以讓我們的例子更加簡單:
fun main() = runBlocking { // this: CoroutineScope
launch { // launch a new coroutine in the scope of runBlocking
delay(1000L)
println("World!")
}
println("Hello,")
}
scope構(gòu)建器
上文講到每一個協(xié)程構(gòu)建器會自動創(chuàng)建一個自己的scope椰棘,這節(jié)給大家介紹一下我們可以通過coroutineScope自定義創(chuàng)建scope。scope的作用就是scope不會結(jié)束在scope內(nèi)部代碼和內(nèi)部launch的所有協(xié)程結(jié)束之前榄笙。
fun main() = runBlocking { // this: CoroutineScope
launch {
delay(200L)
println("Task from runBlocking")
}
coroutineScope { // Creates a coroutine scope
launch {
delay(500L)
println("Task from nested launch")
}
delay(100L)
println("Task from coroutine scope") // This line will be printed before the nested launch
}
println("Coroutine scope is over") // 在我們自定義的coroutineScope結(jié)束之前邪狞,這一行不會執(zhí)行
}
輸出
Task from coroutine scope
Task from runBlocking
Task from nested launch
Coroutine scope is over
runBlocking 和 coroutineScope 比較相似,因為他們都會等待內(nèi)部代碼塊執(zhí)行完畢也會等待其內(nèi)部創(chuàng)建的協(xié)程執(zhí)行完畢茅撞。區(qū)別是一個阻塞當(dāng)前線程一個阻塞當(dāng)前協(xié)程帆卓,所以他們一個是普通方法,一個是suspend方法米丘。
方法拆分重構(gòu)
原文這個章節(jié)的意思沒理解透剑令,只記錄理解的部分:
當(dāng)我們做代碼拆分,把一部分代碼單獨拆分到另一個函數(shù)里時拄查,被拆分出來的函數(shù)需要以suspend修飾吁津,只有suspend修飾的函數(shù)才能在協(xié)程里被調(diào)用。
fun main() = runBlocking {
launch { doWorld() }
println("Hello,")
}
// this is your first suspending function
suspend fun doWorld() {
delay(1000L)
println("World!")
}
總結(jié)
coroutine builder
GlobalScope.launch {}
runBlocking
join
CoroutineScope
Scope Builder
suspend
系列文章快速導(dǎo)航:
java程序員的kotlin課(一):環(huán)境搭建
java程序員的kotlin課(N):coroutines基礎(chǔ)
java程序員的kotlin課(N+1):coroutines 取消和超時
java程序員的kotlin課(N+2):suspending函數(shù)執(zhí)行編排