序言
如果對協程沒有概念,不了解使用協程的好處,請參考《異步編程
》系列文章
引入協程庫
kotlin協程是以一個lib包的形式引入的,參考: kotlinx.coroutines
這里摘錄gradle方式的協程庫引入
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
}
請確認使用了最新版本的kotlin
buildscript {
ext.kotlin_version = '1.3.61'
}
語法講解
此部分翻譯至 https://kotlinlang.org/docs/reference/coroutines/basics.html,作了微調
第一個協程程序
我們使用協程來寫一個helloworld程序
fun main() {
GlobalScope.launch { // 啟動并后臺運行一個協程
delay(1000L) // 非阻塞的暫停1s
println("World!")
}
println("Hello,") // 主線程繼續執行
Thread.sleep(2000L) // 主線程睡眠2秒鐘,保持當前jvm進程存活
}
輸出:
Hello,
World!
本質上,協程是一種輕量級的線程。通過launch
關鍵字啟動,launch
是一個協程構建器
,攜帶一種CoroutineScope的上下文。上面的代碼中,我們在Global Scope中啟動了協程,這意味著,被啟動的協程的生命周期與整個進程保持一致。
我們可以通過把GlobalScope.launch { ... }
替換為thread { ... }
并把delay(...)
替換為Thread.sleep(...)
來實現同樣的效果(去試試吧!)
如果你把 GlobalScope.launch 替換為 thread(譯者:還沒來得及替換delay), 編譯器會報如下的錯:
Error: Kotlin: Suspend functions are only allowed to be called from a coroutine or another suspend function
這是因為delay是一個suspending function
并不會阻塞線程但是會阻塞協程,而且他只能在協程中被調用。
阻塞和非阻塞世界的橋梁
上面的例子在同一段代碼中混用了 non-blocking delay(...)
和 blocking Thread.sleep(...)
. 這樣很容易讓我們混淆哪些是blocking哪些不是blocking的代碼. 讓我們通過 runBlocking 協程構建器來提出阻塞的代碼吧:
fun main() {
GlobalScope.launch { // 啟動并在后臺運行協程
delay(1000L)
println("World!")
}
println("Hello,") // 主線程直接繼續執行
runBlocking { // 會阻塞主線程的執行
delay(2000L) // 保持jvm的持續運行
}
}
上面的代碼執行結果是一樣的,但是只用了非阻塞的delay
。主線程執行了runBlocking
代碼塊,并且主線程在其代碼塊內部執行完成之前會被阻塞。
上面例子里的代碼可以被重寫成更加通用的方式:
fun main() = runBlocking<Unit> { // 啟動主協程
GlobalScope.launch { // 啟動新協程并在后臺允許
delay(1000L)
println("World!")
}
println("Hello,") // 主協程此處直接繼續執行
delay(2000L) // 睡兩秒保持jvm運行
}
這里使用了runBlocking<Unit> { ... }
作為一個適配器,用來在最頂層啟動主協程,我們聲明了Unit作為他的返回值類型,因為main函數需要如此。
這也是對suspending
函數做單元測試的一種方法:
class MyTest {
@Test
fun testMySuspendingFunction() = runBlocking<Unit> {
// here we can use suspending functions using any assertion style that we like
}
}
等待一個任務的執行
上面幾個例子里,都是通過主函數最后睡幾秒來保持jvm進程的運行狀態,這并不是很好,讓我們來更精確的控制時間:
val job = GlobalScope.launch { //此處獲得一個job的引用
delay(1000L)
println("World!")
}
println("Hello,")
job.join() // 主協程等待,直到job執行完畢
現在,執行結果依然相同,但代碼好看多了!
結構化并發
如果想要在實際中使用協程,還需要考慮一些其他的事情。當我們使用GlobalScope.launch
時,我們就創建了top-level的協程。雖然協程是輕量的,但協程運行時畢竟還是要消耗一些內存的。如果我們對新創建的協程沒有保持引用,而協程恰巧hangs住了,如果我們跑了海量的協程并發生內存溢出...必須要手動管理已啟動協程的引用,join 這些協程是容易出錯的。
有更好的解決方案,我們可以在代碼里使用結構化并發:基于當前操作的scope創建所需的協程,而不是使用GlobalScope。
在我們的例子里我們的main函數通過runBlocking協程構建器轉換成一個協程函數。包括runBlocking
在內的每一個協程構造器都會給它對應的代碼塊附加一個CoroutineScope實例。
我們可以基于這個scope創建新的協程,而不需顯示的
join
,因為外層的協程在這個scope內的所有協程介紹之前不會結束。所以我們可以讓我們的例子更加簡單:
fun main() = runBlocking { // this: CoroutineScope
launch { // launch a new coroutine in the scope of runBlocking
delay(1000L)
println("World!")
}
println("Hello,")
}
scope構建器
上文講到每一個協程構建器會自動創建一個自己的scope,這節給大家介紹一下我們可以通過coroutineScope自定義創建scope。scope的作用就是scope不會結束在scope內部代碼和內部launch的所有協程結束之前。
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結束之前,這一行不會執行
}
輸出
Task from coroutine scope
Task from runBlocking
Task from nested launch
Coroutine scope is over
runBlocking 和 coroutineScope 比較相似,因為他們都會等待內部代碼塊執行完畢也會等待其內部創建的協程執行完畢。區別是一個阻塞當前線程一個阻塞當前協程,所以他們一個是普通方法,一個是suspend方法。
方法拆分重構
原文這個章節的意思沒理解透,只記錄理解的部分:
當我們做代碼拆分,把一部分代碼單獨拆分到另一個函數里時,被拆分出來的函數需要以suspend修飾,只有suspend修飾的函數才能在協程里被調用。
fun main() = runBlocking {
launch { doWorld() }
println("Hello,")
}
// this is your first suspending function
suspend fun doWorld() {
delay(1000L)
println("World!")
}
總結
coroutine builder
GlobalScope.launch {}
runBlocking
join
CoroutineScope
Scope Builder
suspend
系列文章快速導航:
java程序員的kotlin課(一):環境搭建
java程序員的kotlin課(N):coroutines基礎
java程序員的kotlin課(N+1):coroutines 取消和超時
java程序員的kotlin課(N+2):suspending函數執行編排