開發環境
- 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~
最后我們來看一下全文的思路