pdf 深入理解kotlin協程_Kotlin協程實現原理:掛起與恢復

41a63d7616871c3d15bcb425b4e19f81.png

今天我們來聊聊Kotlin的協程Coroutine

如果你還沒有接觸過協程,推薦你先閱讀這篇入門級文章What? 你還不知道Kotlin Coroutine?

如果你已經接觸過協程,但對協程的原理存在疑惑,那么在閱讀本篇文章之前推薦你先閱讀下面的文章,這樣能讓你更全面更順暢的理解這篇文章。

Kotlin協程實現原理:Suspend&CoroutineContext

Kotlin協程實現原理:CoroutineScope&Job

Kotlin協程實現原理:ContinuationInterceptor&CoroutineDispatcher

如果你已經接觸過協程,相信你都有過以下幾個疑問:

  1. 協程到底是個什么東西?
  2. 協程的suspend有什么作用,工作原理是怎樣的?
  3. 協程中的一些關鍵名稱(例如:JobCoroutineDispatcherCoroutineContextCoroutineScope)它們之間到底是怎么樣的關系?
  4. 協程的所謂非阻塞式掛起與恢復又是什么?
  5. 協程的內部實現原理是怎么樣的?
  6. ...

接下來的一些文章試著來分析一下這些疑問,也歡迎大家一起加入來討論。

掛起

協程是使用非阻塞式掛起的方式來保證協程運行的。那么什么是非阻塞式掛起呢?下面我們來聊聊掛起到底是一個怎樣的操作。

在之前的文章中提及到suspend關鍵字,它的一個作用是代碼調用的時候會為方法添加一個Continuation類型的參數,保證協程中Continuaton的上下傳遞。

而它另一個關鍵作用是起到掛起協程的標識。

協程運行的時候每遇到被suspend修飾的方法時,都有可能會掛起當前的協程。

注意是有可能。

你可以隨便寫一個方法,該方法也可以被suspend修飾,但這種方法在協程中調用是不會被掛起的。例如

private suspend fun a() {println("aa")
}lifecycleScope.launch {a()
}

因為這種方法是不會返回COROUTINE_SUSPENDED類型的。

協程被掛起的標志是對應的狀態下返回COROUTINE_SUSPENDED標識。

更深入一點的話就涉及到狀態機。協程內部是使用狀態機來管理協程的各個掛起點。

文字有點抽象,具體我們還是來看代碼。我們就拿上面的a方法例子來說明。

首先在Android Studio打開這段代碼的Kotlin Bytecode。可以在Tools -> Kotlin -> Show Kotlin Bytecode中打開。

然后點擊其中的Decompile選項,生成對應的反編譯java代碼。最終代碼如下:

BuildersKt.launch$default((CoroutineScope)LifecycleOwnerKt.getLifecycleScope(this), (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {private CoroutineScope p$;Object L$0;int label;@Nullablepublic final Object invokeSuspend(@NotNull Object $result) {// 掛起標識Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();CoroutineScope $this$launch;switch(this.label) {case 0:ResultKt.throwOnFailure($result);$this$launch = this.p$;MainActivity var10000 = MainActivity.this;// 保存現場this.L$0 = $this$launch;// 設置掛起后恢復時,進入的狀態this.label = 1;// 判斷是否掛起if (var10000.a(this) == var3) {// 掛起,跳出該方法return var3;}// 不需要掛起,協程繼續執行其他邏輯break;case 1:// 恢復現場$this$launch = (CoroutineScope)this.L$0;// 是否需要拋出異常ResultKt.throwOnFailure($result);break;default:throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");}return Unit.INSTANCE;}@NotNullpublic final Continuation create(@Nullable Object value, @NotNull Continuation completion) {Intrinsics.checkParameterIsNotNull(completion, "completion");Function2 var3 = new <anonymous constructor>(completion);var3.p$ = (CoroutineScope)value;return var3;}public final Object invoke(Object var1, Object var2) {return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);}
}), 3, (Object)null);

上面的代碼就是協程的狀態機,通過label來代表不同的狀態,從而對應執行不同case中的邏輯代碼。

在之前的文章中已經介紹過,協程啟動的時候會手動調用一次resumeWith方法,而它對應的內部邏輯就是執行上面的invokeSuspend方法。

所以首次運行協程時label值為0,進入case 0:語句。此時會記錄現場為可能被掛起的狀態做準備,并設置下一個可能被執行的狀態。

如果a方法的返回值為var3,這個var3對應的就是COROUTINE_SUSPENDED。所以只有當a方法返回COROUTINE_SUSPENDED時才會執行if內部語句,跳出方法,此時協程就被掛起。當前線程也就可以執行其它的邏輯,并不會被協程的掛起所阻塞。

所以協程的掛起在代碼層面來說就是跳出協程執行的方法體,或者說跳出協程當前狀態機下的對應狀態,然后等待下一個狀態來臨時在進行執行。

那為什么說我們寫的這個a方法不會被掛起呢?

@Nullable
final Object a(@NotNull Continuation $completion) {return Unit.INSTANCE;
}

原來是它的返回值并不是COROUTINE_SUSPENDED

既然它不會被掛起,那么什么情況下的方法才會被掛起呢?

很簡單,如果我們在a方法中加入delay方法,它就會被掛起。

@Nullable
final Object a(@NotNull Continuation $completion) {Object var10000 = DelayKt.delay(1000L, $completion);return var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED() ? var10000 : Unit.INSTANCE;
}

真正觸發掛起的是delay方法,因為delay方法會創建自己Continuation,同時內部調用getResult方法。

 internal fun getResult(): Any? {installParentCancellationHandler()if (trySuspend()) return COROUTINE_SUSPENDED// otherwise, onCompletionInternal was already invoked & invoked tryResume, and the result is in the stateval state = this.stateif (state is CompletedExceptionally) throw recoverStackTrace(state.cause, this)return getSuccessfulResult(state)}

getResult方法中會通過trySuspend來判斷掛起當前協程。由掛起自身的協程,從而觸發掛起父類的協程。

如果只是為了測試,可以讓a方法直接返回COROUTINE_SUSPENDED

 private suspend fun a(): Any {return kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED}

當然線上千萬不能這樣寫,因為一旦這樣寫協程將一直被掛起,因為你沒有將其恢復的能力。

恢復

現在我們再來聊一聊協程的恢復。

協程的恢復本質是通過ContinuationresumeWith方法來觸發的。

下面我們來看一個可以掛起的例子,通過它來分析協程掛起與恢復的整個流程。

println("main start")
lifecycleScope.launch {println("async start")val b = async {delay(2000)"async"}b.await()println("async end")
}
Handler().postDelayed({println("main end")
}, 1000)

Kotlin代碼很簡單,當前協程運行與主線程中,內部執行一個async方法,通過await方法觸發協程的掛起。

再來看它的對應反編譯java代碼

// 1
String var2 = "main start";
System.out.println(var2);
BuildersKt.launch$default((CoroutineScope)LifecycleOwnerKt.getLifecycleScope(this), (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {private CoroutineScope p$;Object L$0;Object L$1;int label;@Nullablepublic final Object invokeSuspend(@NotNull Object $result) {Object var5 = IntrinsicsKt.getCOROUTINE_SUSPENDED();CoroutineScope $this$launch;Deferred b;switch(this.label) {case 0:// 2ResultKt.throwOnFailure($result);$this$launch = this.p$;String var6 = "async start";System.out.println(var6);b = BuildersKt.async$default($this$launch, (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {private CoroutineScope p$;Object L$0;int label;@Nullablepublic final Object invokeSuspend(@NotNull Object $result) {Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();CoroutineScope $this$async;switch(this.label) {case 0:// 3ResultKt.throwOnFailure($result);$this$async = this.p$;this.L$0 = $this$async;this.label = 1;if (DelayKt.delay(2000L, this) == var3) {return var3;}break;case 1:// 5、6$this$async = (CoroutineScope)this.L$0;ResultKt.throwOnFailure($result);break;default:throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");}return "async";}@NotNullpublic final Continuation create(@Nullable Object value, @NotNull Continuation completion) {Intrinsics.checkParameterIsNotNull(completion, "completion");Function2 var3 = new <anonymous constructor>(completion);var3.p$ = (CoroutineScope)value;return var3;}public final Object invoke(Object var1, Object var2) {return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);}}), 3, (Object)null);this.L$0 = $this$launch;this.L$1 = b;this.label = 1;if (b.await(this) == var5) {return var5;}break;case 1:// 7b = (Deferred)this.L$1;$this$launch = (CoroutineScope)this.L$0;ResultKt.throwOnFailure($result);break;default:throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");}// 8String var4 = "async end";System.out.println(var4);return Unit.INSTANCE;}@NotNullpublic final Continuation create(@Nullable Object value, @NotNull Continuation completion) {Intrinsics.checkParameterIsNotNull(completion, "completion");Function2 var3 = new <anonymous constructor>(completion);var3.p$ = (CoroutineScope)value;return var3;}public final Object invoke(Object var1, Object var2) {return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);}
}), 3, (Object)null);
// 4
(new Handler()).postDelayed((Runnable)null.INSTANCE, 1000L);

有點長,沒關系我們只看關鍵點,看它的狀態機相關的內容。

  1. 首先會輸出main start,然后通過launch創建協程,進入協程狀態機,此時label0,執行case: 0相關邏輯。
  2. 進入case: 0輸出async start,調用async并通過await來掛起當前協程,再掛起的過程中記錄當前掛起點的數據,并將lable設置為1
  3. 進入async創建的協程,此時async協程中的lable0,進入async case: 0執行dealy并掛起async的協程。并將label設置為1。等待2s之后被喚醒。
  4. 此時協程都被掛起,即跳出協程launch方法,執行handler操作。由于post 1s所以比協程中dealy還短,所以會優先輸出main end,然后再過1s,進入恢復協程階段
  5. async中的協程被delay恢復,注意在delay方法中傳入了thisasyncContinuation對象,所以delay內部一旦完成2s計時就會調用ContinuationresumeWith方法來恢復async中的協程,即調用invokeSuspend方法。
  6. 由于被掛起之前已經將async label設置為1,所以進入case: 1,恢復之前掛起的現場,檢查異常,最終返回async
  7. 此時await掛起點被恢復,注意它也傳入了this,對應的就是launch中的Continuation,所以也會回調resumeWith方法,最終調用invokeSuspend,即進入case 1:恢復現場,結束狀態機。
  8. 最后再繼續輸出async end,協程運行結束。

我們可以執行上面的代碼來驗證輸出是否正確

main start
async start
main end
async end

我們來總結一下,協程通過suspend來標識掛起點,但真正的掛起點還需要通過是否返回COROUTINE_SUSPENDED來判斷,而代碼體現是通過狀態機來處理協程的掛起與恢復。在需要掛起的時候,先保留現場與設置下一個狀態點,然后再通過退出方法的方式來掛起協程。在掛起的過程中并不會阻塞當前的線程。對應的恢復通過resumeWith來進入狀態機的下一個狀態,同時在進入下一個狀態時會恢復之前掛起的現場。

本篇文章主要介紹了協程的掛起與恢復原理,同時也分析了協程的狀態機相關的執行過程。希望對學習協程的伙伴們能夠有所幫助,敬請期待后續的協程分析。

項目

android_startup: 提供一種在應用啟動時能夠更加簡單、高效的方式來初始化組件,優化啟動速度。不僅支持Jetpack App Startup的全部功能,還提供額外的同步與異步等待、線程控制與多進程支持等功能。

AwesomeGithub: 基于Github客戶端,純練習項目,支持組件化開發,支持賬戶密碼與認證登陸。使用Kotlin語言進行開發,項目架構是基于Jetpack&DataBindingMVVM;項目中使用了ArouterRetrofitCoroutineGlideDaggerHilt等流行開源技術。

flutter_github: 基于Flutter的跨平臺版本Github客戶端,與AwesomeGithub相對應。

android-api-analysis: 結合詳細的Demo來全面解析Android相關的知識點, 幫助讀者能夠更快的掌握與理解所闡述的要點。

daily_algorithm: 每日一算法,由淺入深,歡迎加入一起共勉。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/258725.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/258725.shtml
英文地址,請注明出處:http://en.pswp.cn/news/258725.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

編譯py-faster-rcnn的問題匯總及解決方法

按照官網 的提示&#xff0c;我開始安裝faster rcnn&#xff0c;但是出現了很多問題&#xff0c;我將其匯總了起來&#xff0c;并提出了解決辦法。 先說明一下我的配置&#xff1a; python : anaconda2linux: centos 6.9 安裝faster rcnn請先參考&#xff1a;《cuda8cudnn4 F…

openWRT自學---針對backfire版本的主要目錄和文件的作用的分析整理

特別說明&#xff1a;要編譯backfire版本&#xff0c;一定要通過svn下載:svn co svn://svn.openwrt.org/openwrt/branches/backfire&#xff0c;而不能使用http://downloads.openwrt.org/backfire/10.03/中的源碼包&#xff1a;backfire_10.03_source.tar.bz2 結合文檔《OpenWr…

自然語言交流系統 phxnet團隊 創新實訓 項目博客 (五)

3DMax方面所涉及的專業知識&#xff1a; &#xff08;1&#xff09;一下的關于3DMax中對于人物的設計和操作均需要在對3DMax基礎知識熟練掌握的情況下進行的。 &#xff08;2&#xff09;骨骼架設&#xff1a;首先對導入到3DMax中的人物模型進行架設骨骼…

linux 安裝python-opencv

三種方法&#xff1a; 1. pip 安裝 &#xff1a; pip install opencv-python &#xff0c;最新版為opencv3安裝后>>> import cv2 >>> print cv2.__version__參考&#xff1a;http://www.cnblogs.com/lclblack/p/6377710.html 2. anaconda的conda安裝 ,可以指…

《你的燈亮著嗎》讀書筆記Ⅲ

轉載于:https://www.cnblogs.com/yue3475975/p/4586220.html

golang協程測試

package main import ( "fmt" "time") const NUMBER 1000000 func test() { for { }} func main() { fmt.Println(time.Now().UnixNano()) for i : 0; i < NUMBER; i { go test() } fmt.Println(time.Now().UnixNano()) for { }} 啟動100W個協程&#…

nvidia顯卡對比分析

本文章轉載自&#xff1a;http://www.cnblogs.com/lijingcong/p/4958617.html 科學計算顯卡的兩個主要性能指標&#xff1a;1、CUDA compute capability&#xff0c;這是英偉達公司對顯卡計算能力的一個衡量指標&#xff1b;2、FLOPS 每秒浮點運算次數&#xff0c;TFLOPS表示每…

零基礎不建議學前端_web前端開發零基礎怎樣入門-哈爾濱前端學習

web前端開發零基礎怎樣入門-哈爾濱前端學習&#xff0c;俗話說&#xff0c;知己知彼&#xff0c;百戰百勝。要想學好web前端&#xff0c;首先要了解什么是web前端&#xff0c;下面由小編來給大家介紹一下&#xff1a;1什么是web&#xff1f;Web就是在Http協議基礎之上, 利用瀏覽…

描述項目的典型用戶與場景

描述項目的典型用戶與場景 名字&#xff1a;小威 年齡&#xff1a;22 職業&#xff1a;學生 收入&#xff1a;無正式收入 知識層次和能力&#xff1a;大學 生活/工作情況&#xff1a;賣東西賺外快 動機&#xff0c;目的&#xff0c;困難&#xff1a;賣東西東西時需要計數 用戶比…

SpringBoot的配置項

2019獨角獸企業重金招聘Python工程師標準>>> spring Boot 其默認是集成web容器的&#xff0c;啟動方式由像普通Java程序一樣&#xff0c;main函數入口啟動。其內置Tomcat容器或Jetty容器&#xff0c;具體由配置來決定&#xff08;默認Tomcat&#xff09;。當然你也可…

北大OJ百練——4075:矩陣旋轉(C語言)

百練的這道題很簡單&#xff0c;通過率也達到了86%&#xff0c;所以我也就來貼個代碼了。。。下面是題目&#xff1a; 不過還是說一下我的思路&#xff1a; 這道題對一個新來說&#xff0c;可能是會和矩陣的轉置相混淆&#xff0c;這題并不是要我們去求矩陣的轉置。 這題&#…

編譯py-faster-rcnn全過程

編譯py-faster-rcnn&#xff0c;花費了好幾天&#xff0c;中間遇到好多問題&#xff0c;今天終于成功編譯。下面詳述我的整個編譯過程。 【注記&#xff1a;】其實下面的依賴庫可以安裝在統一的一個本地目錄下&#xff0c;相關安裝指南&#xff0c;可以參考《深度學習&#xf…

翻譯python語言命令_有道詞典命令行快速翻譯,Python編程的利器

本文的文字及圖片來源于網絡,僅供學習、交流使用,不具有任何商業用途,版權歸原作者所有,如有問題請及時聯系我們以作處理。以下文章來源于Python實用寶典&#xff0c;作者Python實用寶典在編程時經常會遇到需要將中文詞匯翻譯成英文的情況。比如變量名的定義、取一個合適的函數…

不是世界不好,而是你見得太少

轉載于:https://www.cnblogs.com/yymn/p/4590333.html

MonoBehaviour.FixedUpdate 固定更新

function FixedUpdate () : void Description描述 This function is called every fixed framerate frame, if the MonoBehaviour is enabled. 當MonoBehaviour啟用時&#xff0c;其 FixedUpdate 在每一幀被調用。 FixedUpdate should be used instead of Update when dealing …

用Heartbeat實現web服務器高可用

用Heartbeat實現web服務器高可用heartbeat概述: Heartbeat 項目是 Linux-HA 工程的一個組成部分&#xff0c;它實現了一個高可用集群系統。心跳服務和集群通信是高可用集群的兩個關鍵組件&#xff0c;在 Heartbeat 項目里&#xff0c;由 heartbeat 模塊實現了這兩個功能。端口號…

scp創建遠程目錄_在Linux系統中使用Vim讀寫遠程文件

大家好&#xff0c;我是良許。 今天我們討論一個 Vim 使用技巧——用 Vim 讀寫遠程文件。要實現這個目的&#xff0c;我們需要使用到一個叫 netrw.vim 的插件。從 Vim 7.x 開始&#xff0c;netrw.vim 就被設置為默認安裝的標準插件了。這個插件允許用戶通過 ftp、rcp、scp、htt…

softmax logistic loss詳解

softmax函數–softmax layer softmax用于多分類過程中&#xff0c;它將多個神經元的輸出&#xff0c;映射到&#xff08;0,1&#xff09;區間內&#xff0c;可以看成概率來理解&#xff0c;從而來進行多分類&#xff01; 假設我們有一個數組z(z1,z2,...zm),則其softmax函數定…

poj3254 Corn Fields

Description Farmer John has purchased a lush new rectangular pasture composed of M by N (1 ≤ M ≤ 12; 1 ≤ N ≤ 12) square parcels. He wants to grow some yummy corn for the cows on a number of squares. Regrettably, some of the squares are infertile and ca…

Android獲取程序路徑 (/data/data/appname)

Android獲取文件夾路徑 /data/data/ http://www.2cto.com/kf/201301/186614.html String printTxtPath getApplicationContext().getPackageResourcePath() "/files/" fileName;> /data/app/com.example.fileoperation-2.apk/files/printMenu.txt String print…