
前言
隨著kotlin在Android開發領域越來越火,協程在各個專案中的應用也逐漸變得廣泛
但是協程到底是什么呢?
協程其實是個古老的概念,已經非常成熟了,但大家對它的概念一直存在各種疑問,眾說紛紛
有人說協程是輕量級的執行緒,也有人說kotlin協程其實本質是一套執行緒切換方案
顯然這對初學者不太友好,當不清楚一個東西是什么的時候,就很難進入為什么和怎么辦的階段了
本文主要就是回答這個問題,主要包括以下內容
1.關于協程的一些前置知識
2.協程到底是什么?
3.kotlin協程的一些基本概念,掛起函式,CPS轉換,狀態機等
以上問題總結為思維導圖如下:

1. 關于協程的一些前置知識
為了了解協程,我們可以從以下幾個切入點出發
1.什么是行程?為什么要有行程?
2.什么是執行緒?為什么要有執行緒?行程和執行緒有什么區別?
3.什么是協作式,什么是搶占式?
4.為什么要引入協程?是為了解決什么問題?
1.1 什么是行程?
我們在背行程的定義的時候,可能會經常看到一句話
行程是資源分配的最小單位
這個資源分配怎么理解呢?
在單核CPU中,同一時刻只有一個程式在記憶體中被CPU呼叫運行
假設有
A、B兩個程式,A正在運行,此時需要讀取大量輸入資料(IO操作),那么CPU只能干等,直到A資料讀取完畢,再繼續往下執行,A執行完,再去執行程式B,白白浪費CPU資源,
這種方式會浪費CPU資源,我們可能更想要下面這種方式
當程式
A讀取資料的時,切換 到程式B去執行,當A讀取完資料,讓程式B暫停,切換 回程式A執行?
在計算機里 切換 這個名詞被細分為兩種狀態:
掛起:保存程式的當前狀態,暫停當前程式; 激活:恢復程式狀態,繼續執行程式;
這種切換,涉及到了 程式狀態的保存和恢復,而且程式A和B所需的系統資源(記憶體、硬碟等)是不一樣的,那還需要一個東西來記錄程式A和B各自需要什么資源,還有系統控制程式A和B切換,要一個標志來識別等等,所以就有了一個叫 行程的抽象,
1.1.1 行程的定義
行程是一個具有一定獨立功能的程式在一個資料集上的一次動態執行的程序,是作業系統進行資源分配和調度的一個獨立單位,是應用程式運行的載體主要由以下三部分組成:
1.程式:描述行程要完成的功能及如何完成;
2.資料集:程式在執行程序中所需的資源;
3.行程控制塊:記錄行程的外部特征,描述執行變化程序,系統利用它來控制、管理行程,系統感知行程存在的唯一標志,
1.1.2 為什么要有行程
其實上文我們已經分析過了,作業系統之所以要支持多行程,是為了提高CPU的利用率
而為了切換行程,需要行程支持掛起與恢復,不同行程間需要的資源不同,所以這也是為什么行程間資源需要隔離,這也是行程是資源分配的最小單位的原因
1.2 什么是執行緒?
1.2.1 執行緒的定義
輕量級的行程,基本的CPU執行單元,亦是 程式執行程序中的最小單元,由 執行緒ID、程式計數器、暫存器組合和堆疊 共同組成,
執行緒的引入減小了程式并發執行時的開銷,提高了作業系統的并發性能,
1.2.2 為什么要有執行緒?
這個問題也很好理解,行程的出現使得多個程式得以 并發 執行,提高了系統效率及資源利用率,但存在下述問題:
- 單個行程只能干一件事,行程中的代碼依舊是串行執行,
- 執行程序如果堵塞,整個行程就會掛起,即使行程中某些作業不依賴于正在等待的資源,也不會執行,
- 多個行程間的記憶體無法共享,行程間通訊比較麻煩,
執行緒的出現是為了降低背景關系切換消耗,提高系統的并發性,并突破一個行程只能干一件事的缺陷,使得行程內并發成為可能,
1.2.3 行程與執行緒的區別
- 1.一個程式至少有一個行程,一個行程至少有一個執行緒,可以把行程理解做 執行緒的容器;
- 2.行程在執行程序中擁有 獨立的記憶體單元,該行程里的多個執行緒 共享記憶體;
- 3.行程可以拓展到 多機,執行緒最多適合 多核;
- 4.每個獨立執行緒有一個程式運行的入口、順序執行列和程式出口,但不能獨立運行,需依存于應用程式中,由應用程式提供多個執行緒執行控制;
- 5.「行程」是「資源分配」的最小單位,「執行緒」是 「CPU調度」的最小單位
- 6.行程和執行緒都是一個時間段的描述,是
CPU作業時間段的描述,只是顆粒大小不同,
1.3 協作式 & 搶占式
單核CPU,同一時刻只有一個行程在執行,這么多行程,CPU的時間片該如何分配呢?
1.3.1 協作式多任務
早期的作業系統采用的就是協作時多任務,即:由行程主動讓出執行權,如當前行程需等待IO操作,主動讓出CPU,由系統調度下一個行程,
每個行程都循規蹈矩,該讓出CPU就讓出CPU,是挺和諧的,但也存在一個隱患:單個行程可以完全霸占CPU
計算機中的行程良莠不齊,先不說那種居心叵測的行程了,如果是健壯性比較差的行程,運行中途發生了死回圈、死鎖等,會導致整個系統陷入癱瘓!
在這種魚龍混雜的大環境下,把執行權托付給行程自身,肯定是不科學的,于是由作業系統控制的搶占式多任務橫空出世
1.3.2 搶占式多任務
由作業系統決定執行權,作業系統具有從任何一個行程取走控制權和使另一個行程獲得控制權的能力,
系統公平合理地為每個行程分配時間片,行程用完就休眠,甚至時間片沒用完,但有更緊急的事件要優先執行,也會強制讓行程休眠,
這就是所謂的時間片輪轉調度
時間片輪轉調度是一種最古老,最簡單,最公平且使用最廣的演算法,每個行程被分配一個時間段,稱作它的時間片,即該行程允許運行的時間,
如果在時間片結束時行程還在運行,則CPU將被剝奪并分配給另一個行程,如果行程在時間片結束前阻塞或結束,則CPU當即進行切換,調度程式所要做的就是維護一張就緒行程串列,當行程用完它的時間片后,它被移到佇列的末尾,
有了行程設計的經驗,執行緒也做成了搶占式多任務,但也帶來了新的——執行緒安全問題,這個一般通過加鎖的方式來解決,這里就不綴述了,
1.4 為什么要引入協程?
上面介紹行程與執行緒的時候也提到了,之所以引入行程與執行緒是為了異步并發的執行任務,提高系統效率及資源利用率
但作為Java開發者,我們很清楚執行緒并發是多么的危險,寫出來的異步代碼是多么的難以維護,
在Java中,我們一般通過回呼來處理異步任務,但是當異步任務嵌套時,往往程式就會變得很復雜與難維護
舉個例子,當我們需要完成這樣一個需求:查詢用戶資訊 --> 查找該用戶的好友串列 --> 查找該好友的動態
看一下Java回呼的代碼
getUserInfo(new CallBack() {
@Override
public void onSuccess(String user) {
if (user != null) {
System.out.println(user);
getFriendList(user, new CallBack() {
@Override
public void onSuccess(String friendList) {
if (friendList != null) {
System.out.println(friendList);
getFeedList(friendList, new CallBack() {
@Override
public void onSuccess(String feed) {
if (feed != null) {
System.out.println(feed);
}
}
});
}
}
});
}
}
});
這就是傳說中的回呼地獄,如果用kotlin協程實作同樣的需求呢?
val user = getUserInfo()
val friendList = getFriendList(user)
val feedList = getFeedList(friendList)
相比之下,可以說是非常簡潔了
Kotlin 協程的核心競爭力在于:它能簡化異步并發任務,以同步方式寫異步代碼
這也是為什么要引入協程的原因了:簡化異步并發任務
2.到底什么是協程
2.1 什么是協程?
一種非搶占式(協作式)的任務調度模式,程式可以主動掛起或者恢復執行,
2.2 協程與執行緒的區別是什么?
協程基于執行緒,但相對于執行緒輕量很多,可理解為在用戶層模擬執行緒操作;每創建一個協程,都有一個內核態行程動態系結,用戶態下實作調度、切換,真正執行任務的還是內核執行緒,
執行緒的背景關系切換都需要內核參與,而協程的背景關系切換,完全由用戶去控制,避免了大量的中斷參與,減少了執行緒背景關系切換與調度消耗的資源,
執行緒是作業系統層面的概念,協程是語言層面的概念
執行緒與協程最大的區別在于:執行緒是被動掛起恢復,協程是主動掛起恢復
2.3 協程可以怎樣分類?
根據 是否開辟相應的函式呼叫堆疊 又分成兩類:
- 有堆疊協程:有自己的呼叫堆疊,可在任意函式呼叫層級掛起,并轉移調度權;
- 無堆疊協程:沒有自己的呼叫堆疊,掛起點的狀態通過狀態機或閉包等語法來實作;
2.4 Kotlin中的協程是什么?
"假"協程,Kotlin在語言級別并沒有實作一種同步機制(鎖),還是依靠Kotlin-JVM的提供的Java關鍵字(如synchronized),即鎖的實作還是交給執行緒處理
因而Kotlin協程本質上只是一套基于原生Java執行緒池 的封裝,
Kotlin 協程的核心競爭力在于:它能簡化異步并發任務,以同步方式寫異步代碼,
下面介紹一些kotin協程中的基本概念
3. 什么是掛起函式?
我們知道使用suspend關鍵字修飾的函式叫做掛起函式,掛起函式只能在協程體內或者其他掛起函式內使用.
協程內部掛起函式的呼叫處被稱為掛起點,掛起點如果出現異步呼叫,那么當前協程就被掛起,直到對應的Continuation的resume函式被呼叫才會恢復執行
我們下面來看看掛起函式具體執行的細節

可以看出kotlin協程可以做到一行代碼切換執行緒
這些是怎么做到的呢,主要是通過suspend關鍵字
3.1 什么是suspend
suspend 的本質,就是 CallBack,
suspend fun getUserInfo(): String {
withContext(Dispatchers.IO) {
delay(1000L)
}
return "BoyCoder"
}
不過當我們寫掛起函式的時候,并沒有寫callback,所謂的callback從何而來呢?
我們看下反編譯的結果
// Continuation 等價于 CallBack
// ↓
public static final Object getUserInfo(Continuation $completion) {
...
return "BoyCoder";
}
public interface Continuation<in T> {
public val context: CoroutineContext
// 相當于 onSuccess 結果
// ↓ ↓
public fun resumeWith(result: Result<T>)
}
復制代碼
可以看出
1.編譯器會給掛起函式添加一個Continuation引數,這被稱為CPS 轉換(Continuation-Passing-Style Transformation)
2.suspend函式不能在協程體外呼叫的原因也可以知道了,就是因為這個Continuation實體的傳遞
4. 什么是CPS轉換
下面用影片演示掛起函式在 CPS 轉換程序中,函式簽名的變化:

可以看出主要有兩點變化
1.增加了Continuation型別的引數
2.回傳型別從String轉變成了Any
引數的變化我們之前講過,為什么回傳值要變呢?
4.1 掛起函式回傳值
掛起函式經過 CPS 轉換后,它的回傳值有一個重要作用:標志該掛起函式有沒有被掛起,
聽起來有點奇怪,掛起函式還會不掛起嗎?
只要被
suspend修飾的函式都是掛起函式,但是不是所有掛起函式都會被掛起
只有當掛起函式里包含異步操作時,它才會被真正掛起
由于 suspend 修飾的函式,既可能回傳 CoroutineSingletons.COROUTINE_SUSPENDED,表示掛起
也可能回傳同步運行的結果,甚至可能回傳 null為了適配所有的可能性,CPS 轉換后的函式回傳值型別就只能是 Any?了,
4.2 小結
1.suspend修飾的函式就是掛起函式
2.掛起函式,在執行的時候并不一定都會掛起
3.掛起函式只能在其他掛起函式中被呼叫
4.掛起函式里包含異步操作的時候,它才會真正被掛起
5. Continuation是什么?
Continuation詞源是continue,也就是繼續,接下來要做的事的意思
放到程式中Continuation則代表了,接下來要執行的代碼
以上面的代碼為例,當程式運行 getUserInfo() 的時候,它的 Continuation則是下圖紅框的代碼:

Continuation 就是接下來要運行的代碼,剩余未執行的代碼,
理解了 Continuation,以后,CPS就容易理解了,它其實就是:將程式接下來要執行的代碼進行傳遞的一種模式,
而CPS 轉換,就是將原本的同步掛起函式轉換成CallBack 異步代碼的程序,
這個轉換是編譯器在背后做的,我們程式員對此無感知,

當然有人會問,這么簡單粗暴?三個掛起函式最終變成三個 Callback 嗎?
當然不是,思想仍然是CPS的思想,不過需要結合狀態機
CPS與狀態機就是協程實作的核心
6. 狀態機
kotlin協程的實作依賴于狀態機
想要查看其實作,可以將kotin原始碼反編譯成位元組碼來查看編譯后的代碼
關于位元組碼的分析之前已經有很多人做過了,而且做的很好,下面給出狀態機的演示,

- 協程實作的核心就是
CPS變換與狀態機 - 協程執行到掛起函式,一個函式如果被掛起了,它的回傳值會是:
CoroutineSingletons.COROUTINE_SUSPENDED - 掛起函式執行完成后,通過
Continuation.resume方法回呼,這里的Continuation是通過CPS傳入的 - 傳入的
Continuation實際上是ContinuationImpl,resume方法最后會再次回到invokeSuspend方法中 invokeSuspend方法即是我們寫的代碼執行的地方,在協程運行程序中會執行多次invokeSuspend中通過狀態機實作狀態的流轉continuation.label是狀態流轉的關鍵,label改變一次代表協程發生了一次掛起恢復- 通過
break label實作goTo的跳轉效果 - 我們寫在協程里的代碼,被拆分到狀態機里各個狀態中,分開執行
- 每次協程切換后,都會檢查是否發生例外
- 切換協程之前,狀態機會把之前的結果以成員變數的方式保存在
continuation中,
以上是狀態機流轉的大概流程,讀者可跟著參考鏈接,過一下編譯后的位元組碼執行流程后,再來判斷這個流程是否正確
7. CoroutineContext是什么?
我們上面說了Continuation是繼續要執行的代碼,在實作上它也是一個介面
public interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(result: Result<T>)
}
1.Continuation主要由兩部分組成,一個context,一個resumeWith方法
2.通過resumeWith方法執行接下去的代碼
3.通過context獲取背景關銑澩,保存掛起時的一些狀態與資源
CoroutineContext即背景關系,主要承載了資源獲取,配置管理等作業,是執行環境相關的通用資料資源的統一提供者
CoroutineContext是一個特殊的集合,這個集合它既有Map的特點,也有Set的特點
集合的每一個元素都是Element,每個Element都有一個Key與之對應,對于相同Key的Element是不可以重復存在的Element之間可以通過+號組合起來,Element有幾個子類,CoroutineContext也主要由這幾個子類組成:
Job:協程的唯一標識,用來控制協程的生命周期(new、active、completing、completed、cancelling、cancelled);CoroutineDispatcher:指定協程運行的執行緒(IO、Default、Main、Unconfined);CoroutineName: 指定協程的名稱,默認為coroutine;CoroutineExceptionHandler: 指定協程的例外處理器,用來處理未捕獲的例外.
7.1 CoroutineContext的資料結構
先來看看CoroutineContext的全家福

public interface CoroutineContext {
//運算子[]多載,可以通過CoroutineContext[Key]這種形式來獲取與Key關聯的Element
public operator fun <E : Element> get(key: Key<E>): E?
//它是一個聚集函式,提供了從left到right遍歷CoroutineContext中每一個Element的能力,并對每一個Element做operation操作
public fun <R> fold(initial: R, operation: (R, Element) -> R): R
//運算子+多載,可以CoroutineContext + CoroutineContext這種形式把兩個CoroutineContext合并成一個
public operator fun plus(context: CoroutineContext): CoroutineContext
//回傳一個新的CoroutineContext,這個CoroutineContext洗掉了Key對應的Element
public fun minusKey(key: Key<*>): CoroutineContext
//Key定義,空實作,僅僅做一個標識
public interface Key<E : Element>
//Element定義,每個Element都是一個CoroutineContext
public interface Element : CoroutineContext {
//每個Element都有一個Key實體
public val key: Key<*>
//...
}
}
1.CoroutineContext內主要存盤的就是Element,可以通過類似map的 [key] 來取值
2.Element也實作了CoroutineContext介面,這看起來很奇怪,為什么元素本身也是集合呢?主要是為了API設計方便,Element內只會存放自己
3.除了plus方法,CoroutineContext中的其他三個方法都被CombinedContext、Element、EmptyCoroutineContext重寫
4.CombinedContext就是CoroutineContext集合結構的實作,它里面是一個遞回定義,Element就是CombinedContext中的元素,而EmptyCoroutineContext就表示一個空的CoroutineContext,它里面是空實作
7.2 為什么CoroutineContext可以通過+號連接
CoroutineContext能通過+號連接,主要是因為重寫了plus方法
當通過+號連接時,實際上是包裝到了CombinedContext中,并指向上一個Context

如上所示,是一個單鏈表結構,在獲取時也是通過這種方式去查詢對應的key,操作大體邏輯都是先訪問當前element,不滿足,再訪問left的element,順序都是從right到left
最近我整理一些Android 開發相關的學習檔案、面試題,希望能幫助到大家學習提升,如有需要參考的可以點擊鏈接領取**點擊這里免費領取點擊這里免費領取


轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/287765.html
標籤:其他
