Tips 0: 爲簡化表述,本文中「協程」特指「Kotlin 協程」。web
上一篇文章作了不少空洞的理論闡述,來講明協程在概念上應該如何理解,但咱們很難把概念直接用做開發實踐的指導。因此正式使用協程以前,咱們還須要理解代碼的世界中協程是什麼。算法
Kotlin 面向對象的編程語言,線程能夠直接對應成 Thread 對象,但協程中並無 Coroutine 類或接口給咱們使用,使用協程不是很貼近老 Java 人的直覺,是有必定學習成本的。(追到源碼中看的話仍是能找到 AbstractCoroutine 的類型的,是標記爲 InternalCoroutinesApi 的,不該該在外部使用)編程
在實際使用中,協程是一段代碼,在掛起和恢復之間運行。協程的生命週期就是從掛起運行代碼到正常恢復或者異常恢復結束的過程。json
粗略來看,協程有三種狀態(協程運行的過程當中狀態不常常改變,其實能夠更加細分)安全
看起來有些麻煩,但也徹底不麻煩,隨着使用的深刻,會天然而然地記住的。如今咱們只看如何建立協程,新建立的協程在 Incomplete 狀態。markdown
從 Hello World 開始,咱們都學會了 GlobalScope.launch { }
的方式建立一個協程,本節內容就從 launch 開始。併發
coroutineScope.launch
用於建立無返回值的協程,在代碼執行結束後協程就自動結束了。launch 函數的返回值是 Job
類型,能夠經過 Job 獲取協程的狀態、啓動和取消協程以及監聽協程執行完畢。app
coroutineScope.async
是另外一種建立協程的方式,用於建立有返回值的協程,須要主動調用 await() 獲取結果後結束。async 的返回值是 Deferred<T
類型,比起 Job 增長了 await 函數。異步
launch 和 async 的參數相同,都是 CoroutineContext、CoroutineStart 和構成協程內容的 block。CoroutineContext 是協程運行環境,包含了各類協程須要的信息,暫且不展開敘述了。CoroutineStart 用來控制建立的協程如何啓動,默認值的 CoroutineStart.DEFAULT
表示馬上執行,因此大部分示例代碼中並不須要主動調用 start 開始協程。async
舉兩個簡單的栗子看一下吧,首先是一個很是有趣的排序算法「睡眠排序」的協程版本。
private fun sort(nums: IntArray) {
nums.forEach {
lifecycleScope.launch {
delay(it * 10L) // 1ms 容易出現偏差,1s 又過久了,折中
Log.w("CoroutineSampleActivity", "sort $it")
}
}
}
// test
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sort(intArrayOf(10, 4, 2, 1, 9, 3, 5, 8, 6, 7))
}
複製代碼
我在循環中啓動 N 個協程,替代了原算法中的建立 Thread。把 Thread.sleep()
優化成 delay()
應該有一個性能的突破,建議把協程版本命名爲「延遲排序」。launch 建立的協程在代碼執行完畢以後就不用管了,協程也不須要手動釋放。
再看一個 async 的例子吧,此次選一個有用的實踐代碼,咱們讀取一個 asset 的文件,獲取內容字符串。文件 IO 屬於耗時操做,不該該在 UI 線程進行,異步處理正是協程要解決的痛點,看代碼以前能夠先思考一下直接建立線程的寫法應該是什麼樣的。
private suspend fun loadFile(assetName: String): String{
val config = lifecycleScope.async(Dispatchers.IO) {
val inputStream = assets.open(assetName)
return@async inputStream.string() // 是自定義的普通擴展函數,與協程無關
}
return config.await()
}
// test
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
val config = loadFile("config.json")
Log.w("CoroutineSampleActivity", "loadFile $config")
}
}
複製代碼
文件是我隨便寫的,讀到了就好。用協程代替回調和 handler 跨線程傳消息,代碼確實簡潔了不少。
回顧上面的例子,代碼到底在哪些線程呢?讀文件的例子中,咱們爲了避免妨礙 UI 線程指定了 CoroutineContext 爲 Dispatchers.IO,排序的例子則沒有明確提過線程,協程的內容代碼究竟是在哪一個線程執行的呢?這個能夠經過加 Log 的方式查看。
讀文件在一個 worker 線程,其餘代碼都在 main 線程。看得出來,協程中的代碼到底在哪一個線程執行實際上是由 Dispatcher 控制的,若是示例2的代碼中不指定 Dispatchers.IO 的話就影響到 UI 線程了。
涉及併發編程的時候,咱們必須考慮線程安全的問題,在使用協程的時候也一樣須要注意,敲代碼的同時就要在內心梳理好每一行代碼應該在哪一個線程執行。也就是說,使用協程並不意味着不須要理解線程原理。
在協程的官網介紹中,有一段關於協程「輕量」的描述:
import kotlinx.coroutines.*
//sampleStart
fun main() = runBlocking {
repeat(100_000) { // launch a lot of coroutines
launch {
delay(5000L)
print(".")
}
}
}
//sampleEnd
複製代碼
意思是建立十萬個協程同時運行也不會對系統形成負擔,每一個協程都會在 delay 5 秒以後完成打印自動結束。若是同時建立十萬個線程,就幾乎不可能順利運行。我目前並不承認官方的觀點,這種對比的確體現了 Kotlin 中協程與線程的區別,只不過不太能證實「輕量」。
從上面實驗和以前的理論知識來看,delay 不會阻塞當前線程,經過 launch 啓動的十萬協程 delay 後應該都在同一線程輸出。咱們簡化一下,就用 100 個協程加日誌看看效果。
流暢運行。
回到原來的例子,100000 個線程和 2 個線程比較協程固然輕鬆取勝。但實際項目中沒人這樣濫用線程,實現一樣的功能咱們也能夠用 2 個線程實現,協程的優點仍是代碼更好寫。
建立協程並執行已經能實現一些功能了,但還算不上可使用協程,上面簡單略過的 CoroutineContext、CoroutineScope、Job 等都須要更加深刻理解。另外從實踐代碼中咱們都能明顯看到 suspend 的地方,但還未遇到本應成對出現 resume,到底是怎麼回事呢?歡迎關注後續文章。
(一直咕咕咕有點怕了本身了,嘗試一個激勵機制,點贊+評論超過 20 下一篇就在發佈時間起 7 日內更新)