開發環境
- 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
構建器宣告自己的作用域。它會建立一個協程作用域,並且會等待所有已啟動子協程執行完畢。
runBlocking
與 coroutineScope
看起來類似,因為它們都會等待其協程體以及所有子協程結束。主要區別在於:
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~
最後我們來看一下全文的思路