開發環境

  • IntelliJ IDEA 2021.2.2 (Community Edition)
  • Kotlin: 212-1.5.10-release-IJ5284.40

我們已經通過第一個例子學會了啟動協程,這裡介紹一些協程的基礎知識。

阻塞與非阻塞

runBlocking

delay是非阻塞的,Thread.sleep是阻塞的。顯式使用 runBlocking 協程構建器來阻塞。

import kotlinx.coroutines.*

fun main() {
GlobalScope.launch { // 在後臺啟動一個新的協程並繼續
delay(200)
"rustfisher.com".forEach {
print(it)
delay(280)
}
}
println("主執行緒中的程式碼會立即執行")
runBlocking { // 這個表示式阻塞了主執行緒
delay(3000L) //阻塞主執行緒防止過快退出
}
println("\n示例結束")
}

可以看到,runBlocking裡使用了delay來延遲。用了runBlocking的主執行緒會一直阻塞直到runBlocking內部的協程執行完畢。

也就是runBlocking{ delay }實現了阻塞的效果。

我們也可以用runBlocking來包裝主函式。

import kotlinx.coroutines.*

fun main() = runBlocking {
delay(100) // 在這裡可以用delay了 GlobalScope.launch {
delay(100)
println("Fisher")
}
print("Rust ")
delay(3000)
}

runBlocking<Unit>中的<Unit>目前可以省略。

runBlocking也可用在測試中

// 引入junit
dependencies {
implementation("junit:junit:4.13.1")
}

單元測試

使用@Test設定測試

import org.junit.Test
import kotlinx.coroutines.* class C3Test { @Test
fun test1() = runBlocking {
println("[rustfisher] junit測試開始 ${System.currentTimeMillis()}")
delay(1234)
println("[rustfisher] junit測試結束 ${System.currentTimeMillis()}")
}
}

執行結果

[rustfisher] junit測試開始 1632401800686
[rustfisher] junit測試結束 1632401801928

IDEA可能會提示no tasks available。需要把測試選項改為IDEA,如下圖。

等待

有時候需要等待協程執行完畢。可以用join()方法。這個方法會暫停當前的協程,直到執行完畢。需要用main() = runBlocking

import kotlinx.coroutines.*

fun main() = runBlocking {
println("[rustfisher]測試等待")
val job1 = GlobalScope.launch {
println("job1 start")
delay(300)
println("job1 done")
}
val job2 = GlobalScope.launch {
println("job2 start")
delay(800)
println("job2 done")
} job2.join()
job1.join() // 等待
println("測試結束")
}

執行log

[rustfisher]測試等待
job1 start
job2 start
job1 done
job2 done
測試結束

結構化的併發

GlobalScope.launch時,會建立一個頂層協程。之前的例子我們也知道,它不使用主執行緒。新創的協程雖然輕量,但仍會消耗一些記憶體資源。如果忘記保持對新啟動的協程的引用,它還會繼續執行。

我們可以在程式碼中使用結構化併發。

示例中,我們使用runBlocking協程構建器將main函式轉換為協程。在裡面(作用域)啟動的協程不需顯式使用join

觀察下面的例子:

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
println("主執行緒id ${Thread.currentThread().id}")
launch { // 在 runBlocking 作用域中啟動一個新協程1
println("協程1所線上程id ${Thread.currentThread().id}")
delay(300)
println("協程1執行完畢")
}
launch { // 在 runBlocking 作用域中啟動一個新協程2
println("協程2所線上程id ${Thread.currentThread().id}")
delay(500)
println("協程2執行完畢")
}
println("主執行緒執行完畢")
}

執行log

主執行緒id 1
主執行緒執行完畢
協程1所線上程id 1
協程2所線上程id 1
協程1執行完畢
協程2執行完畢

可以看到,不用像之前那樣呼叫Thread.sleep或者delay讓主執行緒等待一段時間,防止虛擬機器退出。

程式會等待它所有的協程執行完畢,然後真正退出。

作用域構建器

使用 coroutineScope 構建器宣告自己的作用域。它會建立一個協程作用域,並且會等待所有已啟動子協程執行完畢。

runBlockingcoroutineScope 看起來類似,因為它們都會等待其協程體以及所有子協程結束。主要區別在於:

  • runBlocking 方法會阻塞當前執行緒來等待,是常規函式
  • coroutineScope 只是掛起,會釋放底層執行緒用於其他用途,是掛起函式

下面這個示例展示了作用域構建器的特點。main是一個作用域。

import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
launch {
delay(200L)
println("協程1 t${Thread.currentThread().id}")
} coroutineScope { // 建立一個協程作用域
launch {
delay(500L)
println("內部協程2-1 t${Thread.currentThread().id}")
} delay(100L)
println("協程2 t${Thread.currentThread().id}")
} println("主任務完畢")
}

執行log

協程2 t1
協程1 t1
內部協程2-1t1
主任務完畢

提取函式重構

launch { …… } 內部的程式碼塊提取到獨立的函式中。提取出來的函式需要 suspend 修飾符,它是掛起函式

import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking fun main() = runBlocking<Unit> {
launch { r1() }
println("DONE")
} // 掛起函式
suspend fun r1() {
delay(300)
println("[rustfisher] 提取出來的函式")
}

log

DONE
[rustfisher] 提取出來的函式

協程是輕量的

我們前面也試過,建立非常多的協程,程式執行OK。

下面的程式碼可以輸出很多的點

import kotlinx.coroutines.*

fun main() = runBlocking {
for (t in 1..10000) {
launch {
delay(t * 500L)
print(".")
}
}
}

全域性協程像守護執行緒

我們在執行緒介紹中知道,如果程序中只剩下了守護執行緒,那麼虛擬機器會退出。

前文那個列印rustfisher.com的例子,其實也能看到,字元沒列印完程式就結束了。

GlobalScope 中啟動的活動協程並不會使程序保活。它們就像守護執行緒。

再舉一個例子

import kotlinx.coroutines.*

fun main() = runBlocking {
GlobalScope.launch {
for (i in 1..1000000) {
delay(200)
println("協程執行: $i")
}
} delay(1000)
println("Bye~")
}

log

協程執行: 1
協程執行: 2
協程執行: 3
協程執行: 4
Bye~

最後我們來看一下全文的思路

參考