深入理解 Kotlin 協程 Coroutine(3)

2021-03-02 Kotlin

前面有兩篇文章介紹過協程,加上這篇,基本上介紹得差不多了~

深入理解 Kotlin Coroutine 

深入理解 Kotlin Coroutine (2)

上周在北京的活動上給大家分享了一下協程,發現大家對於協程最大的困惑是:我為什麼要用它。。。

正好,前面一直想要再寫篇協程的文章,這次讓我們再來回顧一下其中的一些問題。

我們為什麼要用協程?

這個問題對於新接觸協程的朋友來說確實很容易讓人困惑,那麼我們就來看看協程給我們帶來什麼吧。

協程是一種語法糖 協程的出現是來解決異步問題的,但它本身卻不提供異步的能力,額這就很搞笑了,你來自於猴子與逗比嗎?當然不是,協程某種意義上更像是一種語法糖,它為我們隱藏了異步調用和回調的細節,讓我們更關注於業務邏輯的實現。

協程讓代碼更簡潔 協程可以允許我們用同步代碼的方式寫出異步代碼的功能。

async{    val bitmap = await{ loadImage(url) }
}

這是一段 Android 的代碼示例,請注意這個賦值操作,它實際上是切換到 UI 線程之後運行的,而 await 當中的 loadImage(url) 卻是在 IO 線程中運行,所以我們一方面知道協程的異步功能是有線程在後面支持的,另一方面我們也知道異步線程回調可以用協程直接簡化為一個簡單的賦值。

協程讓異步異常處理更方便 如果你的異步代碼出現異常,通常你會在你的回調中加入一個 onError 來傳遞這個異常信息:

interface Callback{    fun onError(e: CertainException)      fun onSuccess(data: Data)
}

而在協程的只支持下,我們只要按照同步代碼的異常捕獲方式進行捕獲就可以了:

async{    try{        val bitmap = await{ loadImage(url) }    }catch(e: CertainException){        ...    }
}

協程更輕量級 通常比較傳統的伺服器實現,一個用戶請求接入後會給他開一個線程來等待和處理它的請求。這樣是非常不經濟的,因為這個用戶有可能就是來逗你玩的,建立連接之後半天來一個字節,就這樣你還得付出一個線程的代價來服務他,尷尬。如果用協程,那麼就要輕量多了,因為協程只是一塊兒內存,不像線程那樣還要對應作業系統內核線程(印象中 Hotpot 的實現就是這樣)。

用協程的理由我個人感覺也就這些了。一句話,協程是一種輕量級的方便操作異步代碼的語法糖,而它本身不提供異步能力。

為什麼說它是語法糖?

async{    val bitmap = await{ loadImage(url) } }

這樣一句代碼其實在編譯完之後會生成一些新的類,async 後面的 Lambda 就會被編譯成一個 CoroutineImpl 類的子類實例,大家只需要按照我經常提到的查看 Kotlin 字節碼的方法去看看就知道了。

那麼這個類究竟是個什麼呢?

abstract class CoroutineImpl(         arity: Int,         @JvmField         protected var completion: Continuation<Any?>? ) : Lambda(arity), Continuation<Any?> {       @JvmField     protected var label: Int = if (completion != null) 0 else -1       private val _context: CoroutineContext? = completion?.context       override val context: CoroutineContext         get() = _context!!       private var _facade: Continuation<Any?>? = null       val facade: Continuation<Any?> get() {         if (_facade == null) _facade = interceptContinuationIfNeeded(_context!!, this)         return _facade!!     }       override fun resume(value: Any?) {         processBareContinuationResume(completion!!) {             doResume(value, null)         }     }       override fun resumeWithException(exception: Throwable) {         processBareContinuationResume(completion!!) {             doResume(null, exception)         }     }       protected abstract fun doResume(data: Any?, exception: Throwable?): Any?       open fun create(completion: Continuation<*>): Continuation<Unit> {         throw IllegalStateException("create(Continuation) has not been overridden")     }       open fun create(value: Any?, completion: Continuation<*>): Continuation<Unit> {         throw IllegalStateException("create(Any?;Continuation) has not been overridden")     } }

它首先是個 Lambda ,這沒毛病。其次,它是一個 Continuation,了解協程的朋友似乎要知道什麼了,沒錯,這貨與我們自己在啟動協程時傳入的 Continuation 實例是同樣的東西,而且我們可以注意到構造方法當中有一個叫做 completion 的欄位,不要驚訝,那就是我們傳入的 Continuation。

實際上,我們通過編譯器編譯出來的字節碼發現,create 方法當中會通過我們的這個 Lambda 表達式創建一個新的 CoroutineImpl 實例,而 doResume 這個抽象方法其實就是我們的 Lambda 表達式的內容了。

在這裡我們還看到了 facade:

val facade: Continuation<Any?> get() {     if (_facade == null) _facade = interceptContinuationIfNeeded(_context!!, this)     return _facade!! }

其中 interceptContinuationIfNeeded 當中就會處理各個攔截器,來完成線程調度或者其他操作,也就是說 facade 返回的 Continuation 實例就是經過類似下面這樣的攔截器返回的實例了:

class UIContext : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {     override fun <T> interceptContinuation(continuation: Continuation<T>)             = ... }

協程是如何啟動的?

我們再來看看協程是如何啟動的。我們啟動協程的時候通常會調用 startCoroutine 或者 createCoroutine,它們都會調用到一個方法:

public fun <T> (suspend () -> T).createCoroutineUnchecked(         completion: Continuation<T> ): Continuation<Unit> =         if (this !is CoroutineImpl)            ...         else             (this.create(completion) as CoroutineImpl).facade

我們已經知道我們的調用者實際上就是一個 CoroutineImpl 的實例,我們只需要關注 else 分支即可,這時候調用我們前面已經見到的 create 方法再創建一個 CoroutineImpl 實例,並把 completion 這個對象傳給它,而 facade 實際上就會觸發攔截器的操作,最終返回的就是經過攔截器處理之後的 Continuation 實例了。

創建完協程之後,就是啟動的邏輯:

public fun <T> (suspend  () -> T).startCoroutine(         completion: Continuation<T> ) {     createCoroutineUnchecked(completion).resume(Unit) }

直接調用 resume 方法,結果怎樣呢?由於攔截器都是我們自己提供的,比較直觀,我們暫且不提,通常情況下,這個 resume 方法最終本質上調用的還是 CoroutineImpl 的 resume 方法:

override fun resume(value: Any?) {     processBareContinuationResume(completion!!) {         doResume(value, null)     } }

internal inline fun processBareContinuationResume(completion: Continuation<*>, block: () -> Any?) {     try {         val result = block()         if (result !== COROUTINE_SUSPENDED) {             (completion as Continuation<Any?>).resume(result)         }     } catch (t: Throwable) {         completion.resumeWithException(t)     } }

processBareContinuationResume 會首先觸發一次 doResume 的調用,這個調用也就是我們自己的協程代碼了,直到遇到第一個 suspend 調用,那麼這時候協程就會被掛起,等待異步操作執行完成之後再來調用我們的 resume/resumeWithException 方法來通知我們數據回來了或者異常發生了。這個過程直到整個協程執行流程結束。

我們稍微關注一下 Continuation 接口:

public interface Continuation<in T> {     public val context: CoroutineContext       public fun resume(value: T)       public fun resumeWithException(exception: Throwable) }

再來看看我們通常的回調版本:

interface Callback<T>{    fun onError(e: Exception)      fun onSuccess(data: T) }

除了協程上下文之外,剩下的兩個方法與我們的回調又有什麼區別呢?

小結

協程是什麼?它就是用來簡化你的異步回調代碼的語法糖!

如果你有興趣加入我們,請直接關注公眾號 Kotlin ,或者加 QQ 群:162452394 (必須正確回答加群暗號哈,防止"特務"混入)聯繫我們。

相關焦點

  • Unity3D協程——線程(Thread)和協程(Coroutine)
    Behavior)參考一下代碼的例子: copyusing UnityEngine;  using System.Collections;    public class Example : MonoBehaviour {      // 保持一個執行腳本的引用    private IEnumerator coroutine
  • 協程中的取消和異常 | 核心概念介紹
    本次系列文章 "協程中的取消和異常" 也是 Android 協程相關的內容,我們將與大家深入探討協程中關於取消操作和異常處理的知識點和技巧。 當我們需要避免多餘的處理來減少內存浪費並節省電量時,取消操作就顯得尤為重要;而妥善的異常處理也是提高用戶體驗的關鍵。
  • 在 Android 開發中使用協程 | 上手指南
    coroutineScopehttps://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.htmlsupervisorScopehttps://kotlin.github.io/kotlinx.coroutines
  • Kotlin協程優雅的與Retrofit纏綿
    作者:limuyang2,連結:https://juejin.im/post/5cfb38f96fb9a07eeb139a00Kotlin已經成為Android開發的Google第一推薦語言,項目中也已經使用了很長時間的kotlin了,加上Kotlin1.3的發布,kotlin協程也已經穩定了
  • 深入理解協程、LiveData 和 Flow
    但協程可以讓開發者只需要一行代碼就完成這個工作,而且沒有累人的回調處理。樣板代碼最少。協程完全活用了 Kotlin 語言的能力,包括 suspend 方法。編寫協程的過程就和編寫普通的代碼塊差不多,編譯器則會幫助開發者完成異步化處理。結構並發性。這個可以理解為針對操作的垃圾收集器,當一個操作不再需要被執行時,協程會自動取消它。
  • 即學即用Kotlin - 協程
    協程作用域協程的作用域有三種,他們分別是:runBlocking:頂層函數,它和 coroutineScope 不一樣,它會阻塞當前線程來等待,所以這個方法在業務中並不適用 。GlobalScope:全局協程作用域,可以在整個應用的聲明周期中操作,且不能取消,所以仍不適用於業務開發。
  • Coroutine從入門到勸退
    比較容易理解:subroutine是只能受讓caller的控制流,return的時候作為callee把控制流交出去。而coroutine可以在任意時刻把控制流交給其他coroutine。多entry point首先,我們要建立一個「context」的概念。
  • Python協程:概念及其用法
    對於協程,我表示其效率確非多線程能比,但本人對此了解並不深入,因此最近幾日參考了一些資料,學習整理了一番,在此分享出來僅供大家參考,如有謬誤請指正,多謝。申明:本文介紹的協程是入門級別,大神請繞道而行,謹防入坑。文章思路:本文將先介紹協程的概念,然後分別介紹Python2.x與3.x下協程的用法,最終將協程與多線程做比較並介紹異步爬蟲模塊。
  • Python與協程從Python2—Python3
    Python3中的協程Gvanrossum希望在Python 3 實現一個原生的基於生成器的協程庫,其中直接內置了對異步IO的支持,這就是asyncio,它在Python 3.4被引入到標準庫。當滿足事件發生的時候,調用相應的協程函數。2)coroutine 協程:協程對象,指一個使用async關鍵字定義的函數,它的調用不會立即執行函數,而是會返回一個協程對象。協程對象需要註冊到事件循環,由事件循環調用。
  • Python 對協程的支持,你得了解下
    協程由於由程序主動控制切換,沒有線程切換的開銷,所以執行效率極高。對於IO密集型任務非常適用,如果是cpu密集型,推薦多進程+協程的方式。在Python3.4之前,官方沒有對協程的支持,存在一些三方庫的實現,比如gevent和Tornado。3.4之後就內置了asyncio標準庫,官方真正實現了協程這一特性。
  • Unity3D協程——協程的執行原理
    接上篇文章Unity3D協程——線程(Thread)和協程(Coroutine)協程是一個分部執行,
  • Python協程與異步編程超全總結
    在asyncio庫中,協程使用@asyncio.coroutine裝飾,使用yield from來驅動,在python3.5中作了如下更改:Python3.8之後 @asyncio.coroutine 裝飾器就會被移除,推薦使用async & await 關鍵字實現協程代碼。
  • Kotlin 一統天下?Kotlin/Native 支持 iOS 和 Web 開發
    此外,值得關注的就是協程這個特性了。雖然協程仍然被標記為實驗性狀態,但官方特意說明了這裡「實驗性」代表的含義。官方表示協程已經完全準備好用於生產環境,他們也已使用協程進行開發,而且也沒發現在使用當中出現任何重大問題。之所以仍保持實驗性狀態,是為了能夠對 Kotlin 繼續進行設計迭代。根據目前的計劃,Kotlin 1.3 將會刪除協程的實驗性狀態。
  • Unity協程性能分析
    中也可以通過啟動協程來迭代這個函數。, Vector3.one, 5)); } private IEnumerator MoveToTarget(Transform obj, Vector3 target, float speed) { }}現在,為了測試它的性能消耗。
  • 利用C語言中的setjmp和longjmp,來實現異常捕獲和協程
    四、利用 setjmp/longjmp 實現協程 1. 什麼是協程在 C 程序中,如果需要並發執行的序列一般都是用線程來實現的,那麼什麼是協程呢?維基百科對於協程的解釋是:3. 協程中的生產者和消費者生產者和消費者在同一個執行序列中執行,通過執行序列的跳轉來交替執行;生產者在生產商品之後,放棄 CPU,讓消費者執行;消費者在消費商品之後,放棄 CPU,讓生產者執行;4.
  • Kotlin 怎麼學 ?遇到過哪些坑?
    kotlin 在 17 年 google io 大會上確定為親兒子,android studio canary 3.0 版本開始,直接支持 kotlin 語言,不需要額外安裝 as plugin。3.語法層面支持懶加載-by lazy 和 lateinit懶加載是經常使用的一個功能,傳統的 java 懶加載,可能是在使用的是判斷一下對象是否為空,但是在 kotlin 裡,提供了語法層次的懶加載,表示該變量在使用之前一定會被初始化。
  • GCC 10 已添加對 C++20 協程的實驗性支持
    GCC 項目的郵件列表記錄顯示,對 C++20 協程的實驗性支持已合併到 GCC 10 編譯器中。
  • Zend-Expressive-Swoole 0.2.2 支持 Swoole 4 協程
    ,現在可以僅通過一個配置即可使整個應用以協程模式運行,完美解決了從前的笨重低性能的印象,基於 Zend Expressive 完美解耦的設計,搭建一個高性能的符合自己心意的框架完全不在話下了! [1] Aura.Router [2] FastRoute [3] Zend Router 2.你想使用哪個依賴注入容器? [1] Aura.Di [2] Pimple [3] Zend ServiceManager 3.你想使用哪個模板引擎?
  • 閒聊 Kotlin-Native (0) - 我們為什麼應該關注一下 Kotlin Native?
    類似的還有協程的設計,語言層面打好基礎你就可以在框架層面造出各種飛機大炮。客觀的講,多平臺相關的絕大多數 API 經過幾輪大規模迭代,已經進入較為穩定的狀態,之所以還稱為 alpha,估計是部分平臺的周邊支持例如 kotlin-js 的 dukat 還在快速迭代當中。多平臺的重大意義在於 Kotlin 生態的建立。
  • 計算機語言協程的歷史、現在和未來
    計算機科學是一門應用科學,幾乎所有概念都是為了理解或解決實際問題而生。協程(Coroutine)的出現也不例外。