## 參考文章
[Kotlin 協程的掛起好神奇好難懂?今天我把它的皮給扒了](https://kaixue.io/kotlin-coroutines-2/)
## 上期回顧
大部分情況下,我們都是用`launch`函數來創建協程,其實還有其他兩個函數也可以用來創建協程:
* `runBlocking`
* `async`
`runBlocking`通常適用于單元測試的場景,而業務開發中不會用到這個函數,因為它是線程阻塞的。
接下來我們主要來對比`launch`與`async`這兩個函數。
* 相同點:它們都可以用來啟動一個協程,返回的都是`Coroutine`,我們這里不需要糾結具體是返回哪個類。
* 不同點:`async`返回的`Coroutine`多實現了`Deferred`接口。
關于`Deferred`更深入的知識就不在這里過多闡述,它的意思就是延遲,也就是結果稍后才能拿到。
我們調用`Deferred.await()`就可以得到結果了。
接下來我們繼續看看`async`是如何使用的,先回憶一下上期中獲取頭像的場景:
~~~kotlin
coroutineScope.launch(Dispatchers.Main) {
// ?? async 函數啟動新的協程
val avatar: Deferred = async { api.getAvatar(user) } // 獲取用戶頭像
val logo: Deferred = async { api.getCompanyLogo(user) } // 獲取用戶所在公司的 logo
// ?? ?? 獲取返回值
show(avatar.await(), logo.await()) // 更新 UI
}
~~~
可以看到 avatar 和 logo 的類型可以聲明為`Deferred`,通過`await`獲取結果并且更新到 UI 上顯示。
`await`函數簽名如下:
~~~kotlin
public suspend fun await(): T
~~~
前面有個關鍵字是之前沒有見過的 ——`suspend`,這個關鍵字就對應了上期最后我們留下的一個問號:協程最核心的那個「非阻塞式」的「掛起」到底是怎么回事?
所以接下來,我們的核心內容就是來好好說一說這個「掛起」。
## 「掛起」的本質
協程中「掛起」的對象到底是什么?掛起線程,還是掛起函數?都不對,**我們掛起的對象是協程。**
還記得協程是什么嗎?**啟動一個協程可以使用`launch`或者`async`函數,協程其實就是這兩個函數中閉包的代碼塊。**
`launch`,`async`或者其他函數創建的**協程,在執行到某一個`suspend`函數的時候,這個協程會被「suspend」,也就是被掛起。**
**那此時又是從哪里掛起?從當前線程掛起。換句話說,就是這個協程從正在執行它的線程上脫離。**
>[success]注意,**不是這個協程停下來了!是脫離,當前線程(協程所在的線程)從這行代碼開始不再運行這個協程了,不再管這個協程要去做什么了**。
suspend 是有暫停的意思,但我們在協程中應該理解為:**當線程執行到協程的 suspend 函數的時候,暫時不繼續執行協程代碼了**。
我們先讓時間靜止,然后兵分兩路,分別看看這兩個互相脫離的線程和協程接下來將會發生什么事情:
### **線程:**
前面我們提到,**掛起會讓協程從正在執行它的線程上脫離**,具體到代碼其實是:
**協程的代碼塊中,線程執行到了 suspend 函數這里的時候,就暫時不再執行剩余的協程代碼,跳出協程的代碼塊。**
那線程接下來會做什么呢?該干嘛干嘛
如果它是一個后臺線程:
* 要么無事可做,被系統回收
* 要么繼續執行別的后臺任務
**總之,跟 Java 線程池里的線程在工作結束之后是完全一樣的:回收或者再利用。**
**如果這個線程它是 Android 的主線程,那它接下來就會繼續回去工作:也就是一秒鐘 60 次的界面刷新任務**。
什么是繼續回去工作?示例如下
~~~kotlin
// 主線程中
GlobalScope.launch(Dispatchers.Main) {
val image = suspendingGetImage(imageId) // 獲取圖片
avatarIv.setImageBitmap(image) // 顯示出來
}
suspend fun suspendingGetImage(id: String) = withContext(Dispatchers.IO) {
...
}
//相當于:
handler.post {
val image = suspendingGetImage(imageId)
avatarIv.setImageBitmap(image)
}
~~~
首先,如果你啟動一個執行主線程任務的協程,它實質上會往你的主線程post()一個新任務Runnable,這個任務Runnable就是你的協程代碼需要完成的任務,那么當這個協程被掛起的時候,那實質上就是你post()的這個任務Runnable提前結束了。那這時候主線程干嘛呢?繼續刷新界面唄。那剩下的代碼怎么辦?協程不是還沒執行完么?剛才也說了,兵分兩路。稍后看協程。
一個常見的場景是,獲取一個圖片,然后顯示出來:
~~~kotlin
// 主線程中
GlobalScope.launch(Dispatchers.Main) {
val image = suspendingGetImage(imageId) // 獲取圖片
avatarIv.setImageBitmap(image) // 顯示出來
}
suspend fun suspendingGetImage(id: String) = withContext(Dispatchers.IO) {
...
}
~~~
這段執行在主線程的協程,它實質上會往你的主線程`post`一個`Runnable`,這個`Runnable`就是你的協程代碼:
~~~kotlin
handler.post {
val image = suspendingGetImage(imageId)
avatarIv.setImageBitmap(image)
}
~~~
當這個協程被掛起的時候,就是主線程`post`的這個`Runnable`提前結束,然后繼續執行它界面刷新的任務。
關于線程,我們就看完了。
這個時候你可能會有一個疑問,那`launch`包裹的剩下代碼怎么辦?協程不是還沒執行完么?剛才也說了,兵分兩路。稍后看協程。
所以接下來,我們來看看協程這一邊。
### **協程:**
*****
線程的代碼在到達`suspend`函數的時候被掐斷,接下來協程會從這個`suspend`函數開始繼續往下執行,不過是在**指定的線程**。
*****
**誰指定的?是`suspend`函數指定的,比如我們這個例子中,函數內部的`withContext`傳入的`Dispatchers.IO`所指定的 IO 線程。** 另外在掛起函數執行完成之后,協程為我們做的最爽的事就來了,**它會自動幫我們把協程再切回來**。
#### **`Dispatchers`調度器小知識**
*****
`Dispatchers`調度器,它可以將協程限制在一個特定的線程執行,或者將它分派到一個線程池,或者讓它不受限制地運行,關于`Dispatchers`這里先不展開了。
那我們平日里常用到的調度器有哪些?
常用的`Dispatchers`,有以下三種:
* `Dispatchers.Main`:Android 中的主線程
* `Dispatchers.IO`:針對磁盤和網絡 IO 進行了優化,適合 IO 密集型的任務,比如:讀寫文件,操作數據庫以及網絡請求
* `Dispatchers.Default`:適合 CPU 密集型的任務,比如計算
*****
回到我們的協程,它從`suspend`函數開始脫離啟動它的線程,繼續執行在`Dispatchers`所指定的 IO 線程。
緊接著在`suspend`函數執行完成之后,協程為我們做的最爽的事就來了:會**自動幫我們把線程再切回來**。
這個「切回來」是什么意思?
我們的協程原本是運行在**主線程**的,當代碼遇到 suspend 函數的時候,發生線程切換,根據`Dispatchers`切換到了 IO 線程;這個所謂的切回來就是:當這個掛起函數執行完畢后,協程會幫我再`post`一個`Runnable`任務,讓我剩下的代碼繼續回到主線程去執行。這就是為啥你指定線程的那個參數不叫`Threads`,而是叫做`Dispatchers`調度器,它不只是只能指定協程執行的線程,還能在suspend掛起函數之后自動再切回來。其實,也不是一定會切回來,也可以通過設置特殊的`Dispatchers`來讓掛起函數執行完之后也不切回來,不過這是你自己的選擇,而不是它的定位。**掛起的定位就是暫時切走,稍后再切回來**。
我們從線程和協程的兩個角度都分析完成后,終于可以對協程的「掛起」suspend 做一個解釋:
**協程在執行到有 suspend 標記的函數的時候,會被 suspend 也就是被掛起,而所謂的被掛起,其實個開啟一個協程一樣,說起來比較玄乎,但其實就是切個線程;**
不過區別在于,**掛起函數在執行完成之后,協程會重新切回它原先的線程**。
再簡單來講,在 Kotlin 中所謂的掛起,其實就是**一個稍后會被自動切回來的線程調度操作**。
>[success] 這個「切回來」的動作,在 Kotlin 里叫做[resume](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines.experimental/-continuation/resume.html),恢復。
那么上期我們最后一個問題,**為什么掛起函數只能在協程里或者另一個掛起函數里面被調用?**
* 首先,通過剛才的分析我們知道:**掛起之后是需要恢復**。而**恢復這個功能是協程的**,如果你不在協程里面調用,恢復這個功能沒法實現。
* 另外,再細想下這個邏輯:一個掛起函數要么在協程里被調用,要么在另一個掛起函數里被調用,那么它其實就是直接或者間接地,總是會在一個協程里被調用的。
所以,**要求`suspend`函數只能在協程里或者另一個 suspend 函數里被調用,還是為了要讓協程能夠在`suspend`函數切換線程之后再切回來**。
## 怎么就「掛起」了?
我們**先了解到了什么是「掛起」后,再接著看看這個「掛起」是怎么做到的**。
首先你可以寫一個自定義的`suspend`函數,然后在主線程上的協程里去調用它,你會發現它還是運行在主線程,沒有切換。
~~~kotlin
suspend fun suspendingPrint(Dispatchers.Main) {
println("Thread: ${Thread.currentThread().name}")
}
launch(){
suspendingPrint()
}
I/System.out: Thread: main
~~~
輸出的結果還是在主線程。沒有切換。
為什么沒切換線程?因為它不知道往哪切,需要我們告訴它。
對比之前例子中`suspendingGetImage`函數代碼:
~~~kotlin
// ??
suspend fun suspendingGetImage(id: String) = withContext(Dispatchers.IO) {
...
}
~~~
我們可以發現不同之處其實在于`withContext`函數。
其實通過`withContext`源碼可以知道,它本身就是一個掛起函數,它接收一個`Dispatcher`參數,依賴這個`Dispatcher`參數的指示,你的協程被掛起,然后切到別的線程。
所以這個`suspend`,其實并不是起到把任何把協程掛起,或者說切換線程的作用。還需要你在掛起函數里面去調用另外一個掛起函數,而且里面這個掛起函數需要是協程自帶的、內部實現了協程掛起代碼的,或者它不是自帶的,但它的內部直接或者間接地調用了某一個自帶的掛起函數,這也是可以的,總之你最終需要調用到一個自帶的掛起函數,讓它來去真正做掛起,也就是線程切換的工作。
**真正掛起協程這件事,是 Kotlin 的協程框架幫我們做的**。也就是說**所謂的協程被掛起或者說切線程這件事,它并不是發生在你外部這個掛起函數被調用的時候,而是里面那個掛起函數,那個`withContext`函數被調用的時候**。
所以我們**想要自己寫一個掛起函數,僅僅只加上`suspend`關鍵字是不行的,還需要函數內部直接或間接地調用到 Kotlin 協程框架自帶的`suspend`函數(掛起)才行**。
>[info]備注:自帶的掛起函數不只是`withContext()`一個,還有其他的,他們都能實現協程的掛起,而我們要想自己寫一個自定義的掛起函數,就需要在這個自定義的掛起函數內部直接或者間接地去調用到某一個自帶的掛起函數才行。
## suspend 的意義?
這個`suspend`關鍵字,**既然它并不是真正實現掛起,那它的作用是什么?**
**它其實是一個提醒。** 誰對誰的提醒?
**函數的創建者對函數的調用者的提醒:我是一個耗時函數,我被我的創建者用掛起的方式放在后臺運行,所以請在協程里調用我。表面上它是一個要求,你需要在協程里調用我,但本質上,它其實是一個提醒——我是一個被自動放在后臺運行的耗時函數,所以你需要在協程里調用我**
這個提醒又有什么作用呢?**它能讓我們的主線程不卡**,對比我們在寫Java代碼時,在主線程做事需要非常小心,一不留神,我們在主線程調用了一個耗時方法,那就會卡一下,而且這種事情是很難避免的。我又不知道哪個方法會耗時?又不是我寫的,就算是我寫的,萬一哪天給忘了呢?而**協程通過掛起函數這種方式,它把耗時任務切線程這個工作,實際上交給了函數的創建者,而不是調用者。對于調用者而言,事情非常簡單,它只會收到一個提醒,你需要把我放在協程里面,剩下的其他調用者都不用管,而通過`suspend`關鍵字這種方式,它實際上作為一個提醒,是形成了一種機制,一種讓所有耗時任務全都自動放在后臺執行的機制,那么主線程是不是就不卡了,所以為什么`suspend`關鍵字并沒有實際去操作掛起,但 Kotlin 卻把它提供出來讓我們使用?因為它本來就不是用來操作掛起的**。
**掛起的操作 —— 也就是切線程,依賴的是掛起函數里面的實際代碼,而不是這個關鍵字**。
所以這個關鍵字,**只是一個提醒**。
還記得剛才我們嘗試自定義掛起函數的方法嗎?
~~~kotlin
// ?? redundant suspend modifier
suspend fun suspendingPrint() {
println("Thread: ${Thread.currentThread().name}")
}
~~~

如果你創建一個`suspend`函數但它內部不包含真正的掛起邏輯,編譯器會給你一個提醒:`redundant suspend modifier`,告訴你這個`suspend`是多余的。
因為你這個函數實質上并沒有發生掛起,那你這個`suspend`關鍵字只有一個效果:就是限制這個函數只能在協程里被調用,如果在非協程的代碼中調用,就會編譯不通過。
所以,**創建一個`suspend`函數,為了讓它包含真正掛起的邏輯,要在它內部直接或間接調用 Kotlin 自帶的`suspend`函數,你的這個`suspend`才是有意義的**。
## 怎么自定義 suspend 函數?
在了解了`suspend`關鍵字的來龍去脈之后,我們就可以進入下一個話題了:怎么自定義`suspend`函數。
這個「怎么自定義」其實分為兩個問題:
* 什么時候需要自定義`suspend`函數?
* 原則:耗時
* 具體該怎么寫呢?
### 什么時候需要自定義 suspend 函數
如果你的某個函數比較耗時,也就是要等的操作,那就把它寫成`suspend`函數。這就是原則。
**耗時操作一般分為兩類:I/O 操作和 CPU 計算工作。比如文件的讀寫、網絡交互、圖片的模糊處理,都是耗時的,通通可以把它們寫進`suspend`函數里。**
**另外這個「耗時」還有一種特殊情況,就是這件事本身做起來并不慢,但它需要等待,比如 5 秒鐘之后再做這個操作。這種也是`suspend`函數的應用場景**。
### 具體該怎么寫
給函數加上`suspend`關鍵字,然后在`withContext`把函數的內容包住就可以了。
提到用`withContext`是因為它在掛起函數里功能最簡單直接:把線程自動切走和切回。
當然并不是只有`withContext`這一個函數來輔助我們實現自定義的`suspend`函數,別的掛起函數功能總會比它多一些或者少一些,比如還有一個掛起函數叫`delay`,它的作用是等待一段時間后再繼續往下執行代碼。
使用它就可以實現剛才提到的等待類型的耗時操作:
~~~kotlin
suspend fun suspendUntilDone() {
while (!done) {
delay(5)
}
}
~~~
這些東西,在我們初步使用協程的時候不用立馬接觸,可以先把協程最基本的方法和概念理清楚。
## 總結
我們今天整個文章其實就在理清一個概念:什么是掛起?**掛起,就是一個稍后會被自動切回來的線程調度操作。**
好,關于協程中的「掛起」我們就解釋到這里。
可能你心中還會存在一些疑惑:
* 協程中掛起的「非阻塞式」到底是怎么回事?
* 協程和 RxJava 在切換線程方面功能是一樣的,都能讓你寫出避免嵌套回調的復雜并發代碼,那協程還有哪些優勢,或者讓開發者使用協程的理由?
這些疑惑的答案,我們都會在下一篇中全部揭曉。
- 前言
- Kotlin簡介
- IntelliJ IDEA技巧總結
- idea設置類注釋和方法注釋模板
- 像Android Studion一樣創建工程
- Gradle
- Gradle入門
- Gradle進階
- 使用Gradle創建一個Kotlin工程
- 環境搭建
- Androidstudio平臺搭建
- Eclipse的Kotlin環境配置
- 使用IntelliJ IDEA
- Kotlin學習路線
- Kotlin官方中文版文檔教程
- 概述
- kotlin用于服務器端開發
- kotlin用于Android開發
- kotlin用于JavaScript開發
- kotlin用于原生開發
- Kotlin 用于數據科學
- 協程
- 多平臺
- 新特性
- 1.1的新特性
- 1.2的新特性
- 1.3的新特性
- 開始
- 基本語法
- 習慣用法
- 編碼規范
- 基礎
- 基本類型
- 包與導入
- 控制流
- 返回與跳轉
- 類與對象
- 類與繼承
- 屬性與字段
- 接口
- 可見性修飾符
- 擴展
- 數據類
- 密封類
- 泛型
- 嵌套類
- 枚舉類
- 對象
- 類型別名
- 內嵌類
- 委托
- 委托屬性
- 函數與Lambda表達式
- 函數
- Lambda表達式
- 內聯函數
- 集合
- 集合概述
- 構造集合
- 迭代器
- 區間與數列
- 序列
- 操作概述
- 轉換
- 過濾
- 加減操作符
- 分組
- 取集合的一部分
- 取單個元素
- 排序
- 聚合操作
- 集合寫操作
- List相關操作
- Set相關操作
- Map相關操作
- 多平臺程序設計
- 平臺相關聲明
- 以Gradle創建
- 更多語言結構
- 解構聲明
- 類型檢測與轉換
- This表達式
- 相等性
- 操作符重載
- 空安全
- 異常
- 注解
- 反射
- 作用域函數
- 類型安全的構造器
- Opt-in Requirements
- 核心庫
- 標準庫
- kotlin.test
- 參考
- 關鍵字與操作符
- 語法
- 編碼風格約定
- Java互操作
- Kotlin中調用Java
- Java中調用Kotlin
- JavaScript
- 動態類型
- kotlin中調用JavaScript
- JavaScript中調用kotlin
- JavaScript模塊
- JavaScript反射
- JavaScript DCE
- 原生
- 并發
- 不可變性
- kotlin庫
- 平臺庫
- 與C語言互操作
- 與Object-C及Swift互操作
- CocoaPods集成
- Gradle插件
- 調試
- FAQ
- 協程
- 協程指南
- 基礎
- 取消與超時
- 組合掛起函數
- 協程上下文與調度器
- 異步流
- 通道
- 異常處理與監督
- 共享的可變狀態與并發
- Select表達式(實驗性)
- 工具
- 編寫kotlin代碼文檔
- 使用Kapt
- 使用Gradle
- 使用Maven
- 使用Ant
- Kotlin與OSGI
- 編譯器插件
- 編碼規范
- 演進
- kotlin語言演進
- 不同組件的穩定性
- kotlin1.3的兼容性指南
- 常見問題
- FAQ
- 與Java比較
- 與Scala比較(官方已刪除)
- Google開發者官網簡介
- Kotlin and Android
- Get Started with Kotlin on Android
- Kotlin on Android FAQ
- Android KTX
- Resources to Learn Kotlin
- Kotlin樣品
- Kotlin零基礎到進階
- 第一階段興趣入門
- kotlin簡介和學習方法
- 數據類型和類型系統
- 入門
- 分類
- val和var
- 二進制基礎
- 基礎
- 基本語法
- 包
- 示例
- 編碼規范
- 代碼注釋
- 異常
- 根類型“Any”
- Any? 可空類型
- 可空性的實現原理
- kotlin.Unit類型
- kotlin.Nothing類型
- 基本數據類型
- 數值類型
- 布爾類型
- 字符型
- 位運算符
- 變量和常量
- 語法和運算符
- 關鍵字
- 硬關鍵字
- 軟關鍵字
- 修飾符關鍵字
- 特殊標識符
- 操作符和特殊符號
- 算術運算符
- 賦值運算符
- 比較運算符
- 邏輯運算符
- this關鍵字
- super關鍵字
- 操作符重載
- 一元操作符
- 二元操作符
- 字符串
- 字符串介紹和屬性
- 字符串常見方法操作
- 字符串模板
- 數組
- 數組介紹創建及遍歷
- 數組常見方法和屬性
- 數組變化以及下標越界問題
- 原生數組類型
- 區間
- 正向區間
- 逆向區間
- 步長
- 類型檢測與類型轉換
- is、!is、as、as-運算符
- 空安全
- 可空類型變量
- 安全調用符
- 非空斷言
- Elvis操作符
- 可空性深入
- 可空性和Java
- 函數
- 函數式編程概述
- OOP和FOP
- 函數式編程基本特性
- 組合與范疇
- 在Kotlin中使用函數式編程
- 函數入門
- 函數作用域
- 函數加強
- 命名參數
- 默認參數
- 可變參數
- 表達式函數體
- 頂層、嵌套、中綴函數
- 尾遞歸函數優化
- 函數重載
- 控制流
- if表達式
- when表達式
- for循環
- while循環
- 循環中的 Break 與 continue
- return返回
- 標簽處返回
- 集合
- list集合
- list集合介紹和操作
- list常見方法和屬性
- list集合變化和下標越界
- set集合
- set集合介紹和常見操作
- set集合常見方法和屬性
- set集合變換和下標越界
- map集合
- map集合介紹和常見操作
- map集合常見方法和屬性
- map集合變換
- 集合的函數式API
- map函數
- filter函數
- “ all ”“ any ”“ count ”和“ find ”:對集合應用判斷式
- 別樣的求和方式:sumBy、sum、fold、reduce
- 根據人的性別進行分組:groupBy
- 扁平化——處理嵌套集合:flatMap、flatten
- 惰性集合操作:序列
- 區間、數組、集合之間轉換
- 面向對象
- 面向對象-封裝
- 類的創建及屬性方法訪問
- 類屬性和字段
- 構造器
- 嵌套類(內部類)
- 枚舉類
- 枚舉類遍歷&枚舉常量常用屬性
- 數據類
- 密封類
- 印章類(密封類)
- 面向對象-繼承
- 類的繼承
- 面向對象-多態
- 抽象類
- 接口
- 接口和抽象類的區別
- 面向對象-深入
- 擴展
- 擴展:為別的類添加方法、屬性
- Android中的擴展應用
- 優化Snackbar
- 用擴展函數封裝Utils
- 解決煩人的findViewById
- 擴展不是萬能的
- 調度方式對擴展函數的影響
- 被濫用的擴展函數
- 委托
- 委托類
- 委托屬性
- Kotlin5大內置委托
- Kotlin-Object關鍵字
- 單例模式
- 匿名類對象
- 伴生對象
- 作用域函數
- let函數
- run函數
- with函數
- apply函數
- also函數
- 標準庫函數
- takeIf 與 takeUnless
- 第二階段重點深入
- Lambda編程
- Lambda成員引用高階函數
- 高階函數
- 內聯函數
- 泛型
- 泛型的分類
- 泛型約束
- 子類和子類型
- 協變與逆變
- 泛型擦除與實化類型
- 泛型類型參數
- 泛型的背后:類型擦除
- Java為什么無法聲明一個泛型數組
- 向后兼容的罪
- 類型擦除的矛盾
- 使用內聯函數獲取泛型
- 打破泛型不變
- 一個支持協變的List
- 一個支持逆變的Comparator
- 協變和逆變
- 第三階段難點突破
- 注解和反射
- 聲明并應用注解
- DSL
- 協程
- 協程簡介
- 協程的基本操作
- 協程取消
- 管道
- 慕課霍丙乾協程筆記
- Kotlin與Java互操作
- 在Kotlin中調用Java
- 在Java中調用Kotlin
- Kotlin與Java中的操作對比
- 第四階段專題練習
- 朱凱Kotlin知識點總結
- Kotlin 基礎
- Kotlin 的變量、函數和類型
- Kotlin 里那些「不是那么寫的」
- Kotlin 里那些「更方便的」
- Kotlin 進階
- Kotlin 的泛型
- Kotlin 的高階函數、匿名函數和 Lambda 表達式
- Kotlin協程
- 初識
- 進階
- 深入
- Kotlin 擴展
- 會寫「18.dp」只是個入門——Kotlin 的擴展函數和擴展屬性(Extension Functions / Properties)
- Kotlin實戰-開發Android