1 它是什麼(協程 和 Kotlin協程) 1.1 協程是什麼 維基百科:協程,英文Coroutine [kəru’tin] (可入廳),是電腦程式的一類組件,推廣了協作式多任務的子程式,允許執行被掛起與被恢復。 作為Google欽定的Android開發首選語言Kotlin,協程並不是 Kotli ...
1 它是什麼(協程 和 Kotlin協程)
1.1 協程是什麼
維基百科:協程,英文Coroutine [kəru’tin] (可入廳),是電腦程式的一類組件,推廣了協作式多任務的子程式,允許執行被掛起與被恢復。
作為Google欽定的Android開發首選語言Kotlin,協程並不是 Kotlin 提出來的新概念,目前有協程概念的編程語言有Lua語言、Python語言、Go語言、C語言等,它只是一種編程思想,不局限於特定的語言。
而每一種編程語言中的協程的概念及實現又不完全一樣,本次分享主要講Kotlin協程。
1.2 Kotlin協程是什麼
Kotlin官網:協程是輕量級線程
可簡單理解:一個線程框架,是全新的處理併發的方式,也是Android上方便簡化非同步執行代碼的方式
類似於 Java:線程池 Android:Handler和AsyncTask,RxJava的Schedulers
註:Kotlin不僅僅是面向JVM平臺的,還有JS/Native,如果用kotlin來寫前端,那Koltin的協程就是JS意義上的協程。如果僅僅JVM 平臺,那確實應該是線程框架。
1.3 進程、線程、協程比較
可通過以下兩張圖理解三者的不同和關係
2 為什麼選擇它(協程解決什麼問題)
非同步場景舉例:
- 第一步:介面獲取當前用戶token及用戶信息
- 第二步:將用戶的昵稱展示界面上
- 第三步:然後再通過這個token獲取當前用戶的消息未讀數
- 第四步:並展示在界面上
2.1 現有方案實現
apiService.getUserInfo().enqueue(object :Callback<User>{
override fun onResponse(call: Call<User>, response: Response<User>) {
val user = response.body()
tvNickName.text = user?.nickName
apiService.getUnReadMsgCount(user?.token).enqueue(object :Callback<Int>{
override fun onResponse(call: Call<Int>, response: Response<Int>) {
val tvUnReadMsgCount = response.body()
tvMsgCount.text = tvUnReadMsgCount.toString()
}
})
}
})
現有方案如何拿到非同步任務的數據,得不到就毀掉哈哈哈,就是通過回調函數來解決。
若嵌套多了,這種畫風是不是有點回調地獄的感覺,俗稱的「callback hell」
2.2 協程實現
mainScope.launch {
val user = apiService.getUserInfoSuspend() //IO線程請求數據
tvNickName.text = user?.nickName //UI線程更新界面
val unReadMsgCount = apiService.getUnReadMsgCountSuspend(user?.token) //IO線程請求數據
tvMsgCount.text = unReadMsgCount.toString() //UI線程更新界面
}
suspend fun getUserInfoSuspend() :User? {
return withContext(Dispatchers.IO){
//模擬網路請求耗時操作
delay(10)
User("asd123", "userName", "nickName")
}
}
suspend fun getUnReadMsgCountSuspend(token:String?) :Int{
return withContext(Dispatchers.IO){
//模擬網路請求耗時操作
delay(10)
10
}
}
紅色框框內的就是一個協程代碼塊。
可以看得出在協程實現中告別了callback,所以再也不會出現回調地獄這種情況了,協程解決了回調地獄
協程可以讓我們用同步的代碼寫出非同步的效果,這也是協程最大的優勢,非同步代碼同步去寫。
小結:協程可以非同步代碼同步去寫,解決回調地獄,讓程式員更方便地處理非同步業務,更方便地切線程,保證主線程安全。
它是怎麼做到的?
3 它是怎麼工作的(協程的原理淺析)
3.1 協程的掛起和恢復
掛起(非阻塞式掛起)
suspend 關鍵字,它是協程中核心的關鍵字,是掛起的標識。
下麵看一下上述示例代碼切換線程的過程:
每一次從主線程切到IO線程都是一次協程的掛起操作;
每一次從IO線程切換主線程都是一次協程的恢復操作;
掛起和恢復是suspend函數特有的能力,其他函數不具備,掛起的內容是協程,不是掛起線程,也不是掛起函數,當線程執行到suspend函數的地方,不會繼續執行當前協程的代碼了,所以它不會阻塞線程,是非阻塞式掛起。
有掛起必然有恢復流程, 恢復是指將已經被掛起的目標協程從掛起之處開始恢復執行。在協程中,掛起和恢復都不需要我們手動處理,這些都是kotlin協程幫我們自動完成的。
那Kotlin協程是如何幫我們自動實現掛起和恢復操作的呢?
它是通過Continuation來實現的。 [kənˌtɪnjuˈeɪʃ(ə)n] (繼續;延續;連續性;後續部分)
3.2 協程的掛起和恢復的工作原理(Continuation)
CPS + 狀態機
Java中沒有suspend函數,suspend是Kotlin中特有的關鍵字,當編譯時,Kotlin編譯器會將含有suspend關鍵字的函數進行一次轉換。
這種被編譯器轉換在kotlin中叫CPS轉換(cotinuation-passing-style)。
轉換流程如下所示
程式員寫的掛起函數代碼:
suspend fun getUserInfo() : User {
val user = User("asd123", "userName", "nickName")
return user
}
假想的一種中間態代碼(便於理解):
fun getUserInfo(callback: Callback<User>): Any? {
val user = User("asd123", "userName", "nickName")
callback.onSuccess(user)
return Unit
}
轉換後的代碼:
fun getUserInfo(cont: Continuation<User>): Any? {
val user = User("asd123", "userName", "nickName")
cont.resume(user)
return Unit
}
我們通過Kotlin生成位元組碼工具查看位元組碼,然後將其反編譯成Java代碼:
@Nullable
public final Object getUserInfo(@NotNull Continuation $completion) {
User user = new User("asd123", "userName", "nickName");
return user;
}
這也驗證了確實是會通過引入一個Continuation對象來實現恢復的流程,這裡的這個Continuation對象中包含了Callback的形態。
它有兩個作用:1. 暫停並記住執行點位;2. 記住函數暫停時刻的局部變數上下文。
所以為什麼我們可以用同步的方式寫非同步代碼,是因為Continuation幫我們做了回調的流程。
下麵看一下這個Continuation 的源碼部分
可以看到這個Continuation中封裝了一個resumeWith的方法,這個方法就是恢復用的。
internal abstract class BaseContinuationImpl() : Continuation<Any?> {
public final override fun resumeWith(result: Result<Any?>) {
//省略好多代碼
invokeSuspend()
//省略好多代碼
}
protected abstract fun invokeSuspend(result: Result<Any?>): Any?
}
internal abstract class ContinuationImpl(
completion: Continuation<Any?>?,
private val _context: CoroutineContext?
) : BaseContinuationImpl(completion) {
protected abstract fun invokeSuspend(result: Result<Any?>): Any?
//invokeSuspend() 這個方法是恢復的關鍵一步
繼續看上述例子:
這是一個CPS之前的代碼:
suspend fun testCoroutine() {
val user = apiService.getUserInfoSuspend() //掛起函數 IO線程
tvNickName.text = user?.nickName //UI線程更新界面
val unReadMsgCount = apiService.getUnReadMsgCountSuspend(user?.token) //掛起函數 IO線程
tvMsgCount.text = unReadMsgCount.toString() //UI線程更新界面
}
當前掛起函數里有兩個掛起函數
通過kotlin編譯器編譯後:
fun testCoroutine(completion: Continuation<Any?>): Any? {
// TestContinuation本質上是匿名內部類
class TestContinuation(completion: Continuation<Any?>?) : ContinuationImpl(completion) {
// 表示協程狀態機當前的狀態
var label: Int = 0
// 兩個變數,對應原函數的2個變數
lateinit var user: Any
lateinit var unReadMsgCount: Int
// result 接收協程的運行結果
var result = continuation.result
// suspendReturn 接收掛起函數的返回值
var suspendReturn: Any? = null
// CoroutineSingletons 是個枚舉類
// COROUTINE_SUSPENDED 代表當前函數被掛起了
val sFlag = CoroutineSingletons.COROUTINE_SUSPENDED
// invokeSuspend 是協程的關鍵
// 它最終會調用 testCoroutine(this) 開啟協程狀態機
// 狀態機相關代碼就是後面的 when 語句
// 協程的本質,可以說就是 CPS + 狀態機
override fun invokeSuspend(_result: Result<Any?>): Any? {
result = _result
label = label or Int.Companion.MIN_VALUE
return testCoroutine(this)
}
}
// ...
val continuation = if (completion is TestContinuation) {
completion
} else {
// 作為參數
// ↓
TestContinuation(completion)
loop = true
while(loop) {
when (continuation.label) {
0 -> {
// 檢測異常
throwOnFailure(result)
// 將 label 置為 1,準備進入下一次狀態
continuation.label = 1
// 執行 getUserInfoSuspend(第一個掛起函數)
suspendReturn = getUserInfoSuspend(continuation)
// 判斷是否掛起
if (suspendReturn == sFlag) {
return suspendReturn
} else {
result = suspendReturn
//go to next state
}
}
1 -> {
throwOnFailure(result)
// 獲取 user 值
user = result as Any
// 準備進入下一個狀態
continuation.label = 2
// 執行 getUnReadMsgCountSuspend
suspendReturn = getUnReadMsgCountSuspend(user.token, continuation)
// 判斷是否掛起
if (suspendReturn == sFlag) {
return suspendReturn
} else {
result = suspendReturn
//go to next state
}
}
2 -> {
throwOnFailure(result)
user = continuation.mUser as Any
unReadMsgCount = continuation.unReadMsgCount as Int
loop = false
}
}
通過一個label標簽控制分支代碼執行,label為0,首先會進入第一個分支,首先將label設置為下一個分支的數值,然後執行第一個suspend方法並傳遞當前Continuation,得到返回值,如果是COROUTINE SUSPENDED,協程框架就直接return,協程掛起,當第一個suspend方法執行完成,會回調Continuation的invokeSuspend方法,進入第二個分支執行,以此類推執行完所有suspend方法。
每一個掛起點和初始掛起點對應的 Continuation 都會轉化為一種狀態,協程恢復只是跳轉到下一種狀態中。掛起函數將執行過程分為多個 Continuation 片段,並且利用狀態機的方式保證各個片段是順序執行的。
小結:協程的掛起和恢復的本質是CPS + 狀態機
4 總結
總結幾個不用協程實現起來很麻煩的騷操作:
- 如果有一個函數,它的返回值需要等到多個耗時的非同步任務都執行完畢返回之後,組合所有任務的返回值作為 最終返回值
- 如果有一個函數,需要順序執行多個網路請求,並且後一個請求依賴前一個請求的執行結果
- 當前正在執行一項非同步任務,但是你突然不想要它執行了,隨時可以取消
- 如果你想讓一個任務最多執行3秒,超過3秒則自動取消
Kotlin協程之所以被認為是假協程,是因為它並不在同一個線程運行,而是真的會創建多個線程。
Kotlin協程在Android上只是一個類似線程池的封裝,真就是一個線程框架。但是它卻可以讓我們用同步的代碼風格寫出非同步的效果,至於怎麼做的,這個不需要我們操心,這些都是kotlin幫我們處理好了,我們需要關心的是怎麼用好它
它就是一個線程框架。
作者:京東物流 王斌
來源:京東雲開發者社區 自猿其說Tech 轉載請註明來源