協程相關基礎
Coroutine 基礎
我們將介紹協程的基本概念。
第一個協程程式
我們把下面的程式碼跑起來:
import kotlinx.coroutines.* fun main() { GlobalScope.launch { // launch new coroutine in background and continue delay(1000L) // non-blocking delay for 1 second (default time unit is ms) println("World!") // print after delay } println("Hello,") // main thread continues while coroutine is delayed Thread.sleep(2000L) // block main thread for 2 seconds to keep JVM alive }
我們可以看到輸出:
Hello, World!
本質上,協程是輕量級的執行緒。可以用協程構造器launch
來啟動協程,使其執行在CoroutineScope
環境上下文中。我們在GlobalScope
啟動一個新的協程,它的生命週期僅受整個應用生命週期的限制。
我們可以嘗試把GlobalScope.launch{...}
替換為thread{...}
,把delay(...)
替換為Thread.sleep(...)
。我們可以得到相同的結果。
假如我們僅僅把GlobalScope.launch
替換為thread
,編譯器將會報錯:
Error: Kotlin: Suspend functions are only allowed to be called from a coroutine or another suspend function
這是因為delay
是特殊的掛起函式,它不會阻塞執行緒,但是這種特殊的函式只能在協程環境中呼叫。
橋接阻塞和非阻塞
上面那個例子混合非阻塞函式delay(...)
和阻塞函式Thread.sleep(...)
在同一段程式碼中,我們不能比較清晰的看出哪個是阻塞的,哪個是非阻塞的。我們讓它執行在runBlocking
協程中:
import kotlinx.coroutines.* fun main() { GlobalScope.launch { // launch new coroutine in background and continue delay(1000L) println("World!") } println("Hello,") // main thread continues here immediately runBlocking {// but this expression blocks the main thread delay(2000L)// ... while we delay for 2 seconds to keep JVM alive } }
這個結果是相同的,但是整段程式碼只使用了非阻塞函式delay()
,主執行緒呼叫runBlocking
,將會阻塞,知道runBlocking
協程內部執完成。
我們可以換種更常見的方式來編寫:
import kotlinx.coroutines.* fun main() = runBlocking<Unit> { // start main coroutine GlobalScope.launch { // launch new coroutine in background and continue delay(1000L) println("World!") } println("Hello,") // main coroutine continues here immediately delay(2000L)// delaying for 2 seconds to keep JVM alive }
上面的例子中,我們明確指定了返回值,Unit
。
等待job完成
使用延時函式來等待協程的完成不是一個好的實現。讓我們使用一種非阻塞的方法明確的等待協程的啟動和執行完成。
import kotlinx.coroutines.* fun main() = runBlocking { //sampleStart val job = GlobalScope.launch { // launch new coroutine and keep a reference to its Job delay(1000L) println("World!") } println("Hello,") job.join() // wait until child coroutine completes //sampleEnd }
現在,這個執行結果是一樣的,但我們的程式碼更加的合理。
結構化併發
在實際應用中我們還有其他要考慮的。我們使用GlobalScope.launch
建立了協程,雖然它是輕量級的,但是它還是會消耗記憶體資源。如果我們忘記引用了,我們新啟動的協程還是會在執行。如果我們的協程裡執行了需要比較長時間的操作,(比如等待很長時間),或者我們啟動了很多的協程,這將會造成記憶體洩漏。手動的保持每個協程的引用是易於出錯的處理的方式。
我們有更合理的解決方法。我們不使用全域性的GlobalScope
協程啟動構造器,我們僅在特定的上下文函式中啟動。
在我們的例子中,我們使用了runBlocking
協程構造器,每一個協程構造器,都將會生成一個上下文環境例項CoroutineScope
。我們可以在該上下文環境例項中啟動一個新的協程,然後就不能明確的使用join
等方法了。因為外層的協程函式不會先結束,除非在它上下文環境中啟動的所有的協程都已經執行完成。因此,我們的例子可以更加簡潔:
import kotlinx.coroutines.* fun main() = runBlocking { // this: CoroutineScope launch { // launch new coroutine in the scope of runBlocking delay(1000L) println("World!") } println("Hello,") }
上下文構造器
此外,有不同的構造器提供協程上下文環境,我們可以使用coroutineScope
構造器來宣告自己的上下文環境。它將會新構建一個等待所有內部協程完成的上下文環境。和runBlocking
主要的不同是,它在等待的時候並不阻塞當前執行緒。
import kotlinx.coroutines.* fun main() = runBlocking { // this: CoroutineScope launch { delay(200L) println("Task from runBlocking") } coroutineScope { // Creates a new coroutine scope launch { delay(500L) println("Task from nested launch") } delay(100L) println("Task from coroutine scope") // This line will be printed before nested launch } println("Coroutine scope is over") // This line is not printed until nested launch completes }
提取函式重構
我們把launch{...}
裡邊的程式碼塊提取出來,如果使用IDE重構函式方式的化,我們可以看到suspend
修飾符已經被新增在函式前邊。掛起函式可以像常規函式一樣在協程上下文中使用。但它具有的特有的特性是,它能夠在協程上下文中掛起,然後在某個時刻恢復到原來的執行點繼續執行。
import kotlinx.coroutines.* fun main() = runBlocking { launch { doWorld() } println("Hello,") } // this is your first suspending function suspend fun doWorld() { delay(1000L) println("World!") }
不建議在提取的方法中再呼叫協程構造器啟動另一個協程上下文環境,因為這樣使得結構不清晰。
協程是輕量化的
import kotlinx.coroutines.* fun main() = runBlocking { repeat(100_000) { // launch a lot of coroutines launch { delay(1000L) print(".") } } }
我們啟動了10萬個協程,每個協程中延時一秒後列印一個.
,如果我們換成執行緒的話,很可能丟擲記憶體溢位錯誤。:(
全域性上下文的協程想守護執行緒
我們使用GlobalScope
每一秒列印一次I'm sleeping
, 然後在主執行緒中延時,然後返回。
import kotlinx.coroutines.* fun main() = runBlocking { //sampleStart GlobalScope.launch { repeat(1000) { i -> println("I'm sleeping $i ...") delay(500L) } } delay(1300L) // just quit after delay //sampleEnd }
我們可以看到列印了三行:
I'm sleeping 0 ... I'm sleeping 1 ... I'm sleeping 2 ...
用GlobalScope
啟動的協程並不儲存程序的存活,它們和守護執行緒相似。