《Kotlin協程》均基於Kotlinx-coroutines 1.3.70
· 協程是什麼?
· 什麼時候用協程?
· 協程的核心是什麼?
· kotlin的協程和其他語言的協程有什麼異同?
協程沒那麼難
協程的出現是爲了解決異步編程中遇到的各種問題。從高級編程語言出現的第一天,異步執行的問題就伴隨出現。
在Kotlin裏使用協程非常方便,
import kotlinx.coroutines.*
fun main() {
GlobalScope.launch { // 在後臺啓動一個新的協程並繼續
delay(1000L) // 非阻塞的等待 1 秒鐘(默認時間單位是毫秒)
println("World!") // 在延遲後打印輸出
}
println("Hello,") // 協程已在等待時主線程還在繼續
Thread.sleep(2000L) // 阻塞主線程 2 秒鐘來保證 JVM 存活
}
上面的代碼是一個常規啓動協程的方式,關鍵函數只有 launch,delay,這兩個函數是kotlin協程獨有的。其他函數都屬於基本庫。
代碼的輸出結果是
Hello,
World!
這是一個典型的異步執行結果,先得到 Hello,而不是按代碼順序先得到 World。
異步執行在平時開發中經常遇到,比如執行一段IO操作,不管是文件讀寫,還是網絡請求,都屬於IO。
在Android中我們對IO操作的一個熟知的規則是不能寫在主線程中,因爲它會卡線程,導致ANR。而上面的代碼其實是不會卡線程的。
用同步的方式寫異步代碼
這句話在很多資料中出現過,劃重點。
理解這句話的關鍵在於,協程幹了什麼,讓這個異步操作不會卡主線程?
我們知道類似的技術在RxJava中也有,它通過手動切線程的方式指定代碼運行所在的線程,從而達到不卡主線程的目的。而協程的高明和簡潔之處在於,開發者不需要主動切線程。
在上面的代碼中打印一下線程名觀察結果。
import kotlinx.coroutines.*
fun main() {
GlobalScope.launch { // 在後臺啓動一個新的協程並繼續
println("Thread: ${Thread.currentThread().name}")
delay(1000L) // 非阻塞的等待 1 秒鐘(默認時間單位是毫秒)
println("World!") // 在延遲後打印輸出
}
println("Thread: ${Thread.currentThread().name}")
println("Hello,") // 協程已在等待時主線程還在繼續
Thread.sleep(2000L) // 阻塞主線程 2 秒鐘來保證 JVM 存活
}
我們會得到
Thread: main
Hello,
Thread: DefaultDispatcher-worker-1
World!
可以看到在打印World的時候,代碼是運行在子線程的。
協程其實沒那麼容易
對於經常用協程開發的人來說,有幾個很有意思的問題值得思考下。
· 上面代碼中的Thread.sleep()可以改成delay()嗎?
· 爲什麼理論上可以開無限多個coroutine?
· 假設有一個IO操作 foo() 耗時a,一個計算密集操作 bar() 耗時b,用協程來執行的話,launc{a b} 耗時c,c是否等於a + b?
另外一個很有意思的問題需要用代碼來展示。在協程中有一個函數 runBlocking{},沒接觸過的可以簡單理解爲它等價於launch{}。
用它來改造上面的代碼,
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
GlobalScope.launch { // 在後臺啓動一個新的協程並繼續
println("Thread: ${Thread.currentThread().name}")
delay(1000L) // 非阻塞的等待 1 秒鐘(默認時間單位是毫秒)
println("World!") // 在延遲後打印輸出
}
println("Thread: ${Thread.currentThread().name}")
println("Hello,") // 協程已在等待時主線程還在繼續
Thread.sleep(2000L) // 阻塞主線程 2 秒鐘來保證 JVM 存活
}
我們會得到
Thread: DefaultDispatcher-worker-1
Thread: main
Hello,
World!
現在我們把 GlobalScope.launch這行改造一下,
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
launch { // 在後臺啓動一個新的協程並繼續
println("Thread: ${Thread.currentThread().name}")
delay(1000L) // 非阻塞的等待 1 秒鐘(默認時間單位是毫秒)
println("World!") // 在延遲後打印輸出
}
println("Thread: ${Thread.currentThread().name}")
println("Hello,") // 協程已在等待時主線程還在繼續
Thread.sleep(2000L) // 阻塞主線程 2 秒鐘來保證 JVM 存活
}
現在再看執行結果,
Thread: main
Hello,
Thread: main
World!
WTF? launch裏的代碼也執行在主線程了?
這個問題涉及到Kotlin協程的Scope,調度,也是協程的實現核心邏輯
Kotlin不是第一個提出協程的
實際上在Kotlin之前就有不少語言實踐了協程這個概念。比如python,golang。
而最原始的協程其實不叫協程,叫纖程(Fiber)。聽說過Fiber的人都已經。。
甲:聽說過纖程嗎
乙:Fiber是吧
甲:你今年起碼40歲了吧
纖程是微軟第一個提出的,但因爲它的使用非常的反人類,對程序員的代碼質量要求非常高,以至於沒人願意用它。雖然現在還可以在微軟官網上找到關於纖程的資料,但能用好纖程的程序員鳳毛麟角。
直到golang的出現,才把協程這個技術發揚光大。有人說python也有協程呀,爲什麼是golang。
其實python的協程不是真正意義上的協程,後面我們會說到。python的協程是基於yield關鍵字進行二次封裝的,雖然在高層抽象上也是以函數作爲協程粒度,但對比golang差的太遠。
golang做了什麼
golang的協程叫goroutine,跟kotlin的coroutine差不多。golang用一種程序員更容易理解的抽象定義了協程粒度goroutine,還有它的各種操作。
對於程序員來說,再也不用關心什麼時候切協程,協程在什麼線程運行這種問題,開發效率和代碼運行效率得到成倍提升。
golang在編譯器上做了很多優化,當代碼中發生IO或者內核中斷的時候,會自動幫你切協程。熟悉計算機原理的能明白,當發生內核中斷的時候,比如請求一個磁盤文件,中斷髮生時CPU其實是沒有工作的,執行邏輯在這個時候處於一個空轉,直到中斷返回結果才繼續往下執行。
於是在中斷髮生的時候,CPU相當於浪費了一段時間。golang在這個時候切協程,就能把CPU浪費的算力利用起來交給另外一個協程去執行。
kotlin的協程還在發展
如果去看kotlin的協程源碼的話會發現裏面有很多 exeprimental 的api和實現邏輯。直到1.3.70爲止,jetbain團隊還在繼續地爲coroutine機制增加新的活力。目前來說coroutine處於一個穩定階段,可以基於1.3.70版本來分析它,後面應該不會有很大機制上的變動了。