Kotlin協程基礎知識, 一篇搞懂.html
Coroutines(協程), 計算機程序組件, 經過容許任務掛起和恢復執行, 來支持非搶佔式的多任務. (見Wiki).android
協程主要是爲了異步, 非阻塞的代碼. 這個概念並非Kotlin特有的, Go, Python等多個語言中都有支持.git
Kotlin中用協程來作異步和非阻塞任務, 主要優勢是代碼可讀性好, 不用回調函數. (用協程寫的異步代碼乍一看很像同步代碼.)github
Kotlin對協程的支持是在語言級別的, 在標準庫中只提供了最低程度的APIs, 而後把不少功能都代理到庫中.promise
Kotlin中只加了suspend
做爲關鍵字. async
和await
不是Kotlin的關鍵字, 也不是標準庫的一部分.安全
比起futures和promises, kotlin中suspending function
的概念爲異步操做提供了一種更安全和不易出錯的抽象.bash
kotlinx.coroutines
是協程的庫, 爲了使用它的核心功能, 項目須要增長kotlinx-coroutines-core
的依賴.網絡
先上一段官方的demo:架構
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
fun main() {
GlobalScope.launch { // launch a 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, 延遲1s以後, 打印World.併發
對這段代碼的解釋:
launch
開始了一個計算, 這個計算是可掛起的(suspendable), 它在計算過程當中, 釋放了底層的線程, 當協程執行完成, 就會恢復(resume).
這種可掛起的計算就叫作一個協程(coroutine). 因此咱們能夠簡單地說launch
開始了一個新的協程.
注意, 主線程須要等待協程結束, 若是註釋掉最後一行的Thread.sleep(2000L)
, 則只打印Hello, 沒有World.
coroutine(協程)能夠理解爲輕量級的線程. 多個協程能夠並行運行, 互相等待, 互相通訊. 協程和線程的最大區別就是協程很是輕量(cheap), 咱們能夠建立成千上萬個協程而沒必要考慮性能.
協程是運行在線程上能夠被掛起的運算. 能夠被掛起, 意味着運算能夠被暫停, 從線程移除, 存儲在內存裏. 此時, 線程就能夠自由作其餘事情. 當計算準備好繼續進行時, 它會返回線程(但不必定要是同一個線程).
默認狀況下, 協程運行在一個共享的線程池裏, 線程仍是存在的, 只是一個線程能夠運行多個協程, 因此線程不必太多.
在上面的代碼中加上線程的名字:
fun main() {
GlobalScope.launch {
// launch a new coroutine in background and continue
delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
println("World! + ${Thread.currentThread().name}") // print after delay
}
println("Hello, + ${Thread.currentThread().name}") // main thread continues while coroutine is delayed
Thread.sleep(2000L) // block main thread for 2 seconds to keep JVM alive
}
複製代碼
能夠在IDE的Edit Configurations中設置VM options: -Dkotlinx.coroutines.debug
, 運行程序, 會在log中打印出代碼運行的協程信息:
Hello, + main
World! + DefaultDispatcher-worker-1 @coroutine#1
複製代碼
上面例子中的delay
方法是一個suspend function
. delay()
和Thread.sleep()
的區別是: delay()
方法能夠在不阻塞線程的狀況下延遲協程. (It doesn't block a thread, but only suspends the coroutine itself). 而Thread.sleep()
則阻塞了當前線程.
因此, suspend的意思就是協程做用域被掛起了, 可是當前線程中協程做用域以外的代碼不被阻塞.
若是把GlobalScope.launch
替換爲thread
, delay方法下面會出現紅線報錯:
Suspend functions are only allowed to be called from a coroutine or another suspend function
複製代碼
suspend方法只能在協程或者另外一個suspend方法中被調用.
在協程等待的過程當中, 線程會返回線程池, 當協程等待結束, 協程會在線程池中一個空閒的線程上恢復. (The thread is returned to the pool while the coroutine is waiting, and when the waiting is done, the coroutine resumes on a free thread in the pool.)
啓動一個新的協程, 經常使用的主要有如下幾種方式:
launch
async
runBlocking
它們被稱爲coroutine builders
. 不一樣的庫能夠定義其餘更多的構建方式.
runBlocking
用來鏈接阻塞和非阻塞的世界.
runBlocking
能夠創建一個阻塞當前線程的協程. 因此它主要被用來在main函數中或者測試中使用, 做爲鏈接函數.
好比前面的例子能夠改寫成:
fun main() = runBlocking<Unit> {
// start main coroutine
GlobalScope.launch {
// launch a new coroutine in background and continue
delay(1000L)
println("World! + ${Thread.currentThread().name}")
}
println("Hello, + ${Thread.currentThread().name}") // main coroutine continues here immediately
delay(2000L) // delaying for 2 seconds to keep JVM alive
}
複製代碼
最後再也不使用Thread.sleep()
, 使用delay()
就能夠了. 程序輸出:
Hello, + main @coroutine#1
World! + DefaultDispatcher-worker-1 @coroutine#2
複製代碼
上面的例子delay了一段時間來等待一個協程結束, 不是一個好的方法.
launch
返回Job
, 表明一個協程, 咱們能夠用Job
的join()
方法來顯式地等待這個協程結束:
fun main() = runBlocking {
val job = GlobalScope.launch {
// launch a new coroutine and keep a reference to its Job
delay(1000L)
println("World! + ${Thread.currentThread().name}")
}
println("Hello, + ${Thread.currentThread().name}")
job.join() // wait until child coroutine completes
}
複製代碼
輸出結果和上面是同樣的.
Job
還有一個重要的用途是cancel()
, 用於取消再也不須要的協程任務.
async
開啓線程, 返回Deferred<T>
, Deferred<T>
是Job
的子類, 有一個await()
函數, 能夠返回協程的結果.
await()
也是suspend函數, 只能在協程以內調用.
fun main() = runBlocking {
// @coroutine#1
println(Thread.currentThread().name)
val deferred: Deferred<Int> = async {
// @coroutine#2
loadData()
}
println("waiting..." + Thread.currentThread().name)
println(deferred.await()) // suspend @coroutine#1
}
suspend fun loadData(): Int {
println("loading..." + Thread.currentThread().name)
delay(1000L) // suspend @coroutine#2
println("loaded!" + Thread.currentThread().name)
return 42
}
複製代碼
運行結果:
main @coroutine#1
waiting...main @coroutine#1
loading...main @coroutine#2
loaded!main @coroutine#2
42
複製代碼
看一下launch
方法的聲明:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
...
}
複製代碼
其中有幾個相關概念咱們要了解一下.
協程老是在一個context下運行, 類型是接口CoroutineContext
. 協程的context是一個索引集合, 其中包含各類元素, 重要元素就有Job
和dispatcher. Job
表明了這個協程, 那麼dispatcher是作什麼的呢?
構建協程的coroutine builder: launch
, async
, 都是CoroutineScope
類型的擴展方法. 查看CoroutineScope
接口, 其中含有CoroutineContext
的引用. scope是什麼? 有什麼做用呢?
下面咱們就來回答這些問題.
Context中的CoroutineDispatcher
能夠指定協程運行在什麼線程上. 能夠是一個指定的線程, 線程池, 或者不限.
看一個例子:
fun main() = runBlocking<Unit> {
launch {
// context of the parent, main runBlocking coroutine
println("main runBlocking : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Unconfined) {
// not confined -- will work with main thread
println("Unconfined : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Default) {
// will get dispatched to DefaultDispatcher
println("Default : I'm working in thread ${Thread.currentThread().name}")
}
launch(newSingleThreadContext("MyOwnThread")) {
// will get its own new thread
println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
}
}
複製代碼
運行後打印出:
Unconfined : I'm working in thread main Default : I'm working in thread DefaultDispatcher-worker-1
newSingleThreadContext: I'm working in thread MyOwnThread main runBlocking : I'm working in thread main
複製代碼
API提供了幾種選項:
Dispatchers.Default
表明使用JVM上的共享線程池, 其大小由CPU核數決定, 不過即使是單核也有兩個線程. 一般用來作CPU密集型工做, 好比排序或複雜計算等.Dispatchers.Main
指定主線程, 用來作UI更新相關的事情. (須要添加依賴, 好比kotlinx-coroutines-android
.) 若是咱們在主線程上啓動一個新的協程時, 主線程忙碌, 這個協程也會被掛起, 僅當線程有空時會被恢復執行.Dispatchers.IO
: 採用on-demand建立的線程池, 用於網絡或者是讀寫文件的工做.Dispatchers.Unconfined
: 不指定特定線程, 這是一個特殊的dispatcher.若是不明確指定dispatcher, 協程將會繼承它被啓動的那個scope的context(其中包含了dispatcher).
在實踐中, 更推薦使用外部scope的dispatcher, 由調用方決定上下文. 這樣也方便測試.
newSingleThreadContext
建立了一個線程來跑協程, 一個專一的線程算是一種昂貴的資源, 在實際的應用中須要被釋放或者存儲複用.
切換線程還能夠用withContext
, 能夠在指定的協程context下運行代碼, 掛起直到它結束, 返回結果. 另外一種方式是新啓一個協程, 而後用join
明確地掛起等待.
在Android這種UI應用中, 比較常見的作法是, 頂部協程用CoroutineDispatchers.Main
, 當須要在別的線程上作一些事情的時候, 再明確指定一個不一樣的dispatcher.
當launch
, async
或runBlocking
開啓新協程的時候, 它們自動建立相應的scope. 全部的這些方法都有一個帶receiver的lambda參數, 默認的receiver類型是CoroutineScope
.
IDE會提示this: CoroutineScope
:
launch { /* this: CoroutineScope */
}
複製代碼
當咱們在runBlocking
, launch
, 或async
的大括號裏面再建立一個新的協程的時候, 自動就在這個scope裏建立:
fun main() = runBlocking {
/* this: CoroutineScope */
launch { /* ... */ }
// the same as:
this.launch { /* ... */ }
}
複製代碼
由於launch
是一個擴展方法, 因此上面例子中默認的receiver是this
. 這個例子中launch
所啓動的協程被稱做外部協程(runBlocking
啓動的協程)的child. 這種"parent-child"的關係經過scope傳遞: child在parent的scope中啓動.
協程的父子關係:
因此, 關於scope目前有兩個關鍵知識點:
CoroutineScope
裏.協程的父子關係有如下兩個特性:
值得注意的是, 也能夠不啓動協程就建立一個新的scope. 建立scope能夠用工廠方法: MainScope()
或CoroutineScope()
.
coroutineScope()
方法也能夠建立scope. 當咱們須要以結構化的方式在suspend函數內部啓動新的協程, 咱們建立的新的scope, 自動成爲suspend函數被調用的外部scope的child.
因此上面的父子關係, 能夠進一步抽象到, 沒有parent協程, 由scope來管理其中全部的子協程. (注意: 實際上scope會提供默認job, cancel
操做是由scope中的job支持的.)
Scope在實際應用中解決什麼問題呢? 若是咱們的應用中, 有一個對象是有本身的生命週期的, 可是這個對象又不是協程, 好比Android應用中的Activity, 其中啓動了一些協程來作異步操做, 更新數據等, 當Activity被銷燬的時候須要取消全部的協程, 來避免內存泄漏. 咱們就能夠利用CoroutineScope
來作這件事: 建立一個CoroutineScope
對象和activity的生命週期綁定, 或者讓activity實現CoroutineScope
接口.
因此, scope的主要做用就是記錄全部的協程, 而且能夠取消它們.
A CoroutineScope keeps track of all your coroutines, and it can cancel all of the coroutines started in it.
複製代碼
這種利用scope將協程結構化組織起來的機制, 被稱爲"structured concurrency". 好處是:
經過這種結構化的併發模式: 咱們能夠在建立top級別的協程時, 指定主要的context一次, 全部嵌套的協程會自動繼承這個context, 只在有須要的時候進行修改便可.
GlobalScope
啓動的協程都是獨立的, 它們的生命只受到application的限制. 即GlobalScope
啓動的協程沒有parent, 和它被啓動時所在的外部的scope沒有關係.
launch(Dispatchers.Default) { ... }
和GlobalScope.launch { ... }
用的dispatcher是同樣的.
GlobalScope
啓動的協程並不會保持進程活躍. 它們就像daemon threads(守護線程)同樣, 若是JVM發現沒有其餘通常的線程, 就會關閉.
第三方博客: