Kotlin Coroutine 探索之旅

協(xié)程

大家如果已經(jīng)使用Kotlin語言進(jìn)行開發(fā),對協(xié)程這個概念應(yīng)該不會很陌生。雖然在網(wǎng)上有很多Kotlin協(xié)程相關(guān)的文章,但當(dāng)我開始準(zhǔn)備使用的時候,還是有如下幾個疑慮。

  1. 協(xié)程到底能夠解決什么樣的問題?
  2. 協(xié)程和我們常用的Executor、RxJava有什么區(qū)別?
  3. 項目上使用有什么風(fēng)險嗎?

接下來就帶著這幾個問題一起來揭開協(xié)程神秘的面紗。

如何使用

關(guān)于協(xié)程,我在網(wǎng)上看到最多的說法是協(xié)程是輕量級的線程。那么協(xié)程首先應(yīng)該解決的問題就是程序中我們常常遇到的 “異步” 的問題。我們看看官網(wǎng)介紹的幾個使用例子。

依賴

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'

入門

import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch { // 在后臺啟動一個新的協(xié)程并繼續(xù)
        delay(1000L)
        println("World!")
    }
    println("Hello,") // 主線程中的代碼會立即執(zhí)行
    runBlocking {     // 但是這個表達(dá)式阻塞了主線程
        delay(2000L)  // ……我們延遲 2 秒來保證 JVM 的存活
    } 
}

掛起函數(shù)


suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // 假設(shè)我們在這里做了一些有用的事
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // 假設(shè)我們在這里也做了一些有用的事
    return 29
}

val time = measureTimeMillis {
    val one = doSomethingUsefulOne()
    val two = doSomethingUsefulTwo()
    println("The answer is ${one + two}")
}
println("Completed in $time ms")

結(jié)果:

The answer is 42
Completed in 2015 ms

使用 async 并發(fā)

val time = measureTimeMillis {
    val one = async { doSomethingUsefulOne() }
    val two = async { doSomethingUsefulTwo() }
    println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")

結(jié)果:

The answer is 42
Completed in 1017 ms

單元測試

class MyTest {
    @Test
    fun testMySuspendingFunction() = runBlocking<Unit> {
        // 這里我們可以使用任何喜歡的斷言風(fēng)格來使用掛起函數(shù)
    }
}

更新詳細(xì)的使用可參考官網(wǎng)示例

為何使用

既然已經(jīng)有這么多異步處理的框架,那我們?yōu)楹芜€要使用協(xié)程。這里舉個例子,看看對同個需求,不同異步框架的處理方式。

現(xiàn)在有一個產(chǎn)品需求,生成一個二維碼在頁面展示給用戶。我們來對比看看不同的做法。

Thread

Thread(Runnable {
        try {
            val qrCode: Bitmap =
            CodeCreator.createQRCode(this@ShareActivity, SHARE_QR_CODE)
            runOnUiThread { 
                img_qr_code.setImageBitmap(qrCode)
                }
            } catch (e: WriterException) {
                e.printStackTrace()
            }
        }).start()
    }

Executors

Executors.newSingleThreadExecutor().execute {
        try {
            val qrCode: Bitmap =
            CodeCreator.createQRCode(this@ShareActivity, SHARE_QR_CODE)
            runOnUiThread {
                img_qr_code.setImageBitmap(qrCode)
            }
        } catch (e: WriterException) {
            e.printStackTrace()
        }
    }

RxJava

Observable.just(SHARE_QR_CODE)
        .map(new Function<String, Bitmap>() {
            @Override
            public Bitmap apply(String s) throws Exception {
                return CodeCreator.createQRCode(ShareActivity.this, s);
            }
        })
        .subscribe(new Consumer<Bitmap>() {
            @Override
            public void accept(Bitmap bitmap) throws Exception {
                img_qr_code.setImageBitmap(bitmap);
            }
        });

Koroutine

 val job = GlobalScope.launch(Dispatchers.IO) {
            val bitmap = CodeCreator.createQRCode(ShareActivity.this, SHARE_QR_CODE)
            launch(Dispatchers.Main) {
                img_qr_code.setImageBitmap(bitmap)
            }
        }
}

通過這個例子,可以看出使用協(xié)程的非常方便解決 "異步回調(diào)" 問題。
相比傳統(tǒng)的Thread及Excutors,RxJava將嵌套回調(diào)轉(zhuǎn)換成鏈?zhǔn)秸{(diào)用的形式,提高了代碼可讀性。協(xié)程直接將鏈?zhǔn)秸{(diào)用轉(zhuǎn)換成了協(xié)程內(nèi)的順序調(diào)用,"代碼更加精簡"

性能

官網(wǎng)上對于協(xié)程的有一句介紹。

本質(zhì)上,協(xié)程是輕量級的線程

那么協(xié)程的執(zhí)行效率到底怎么樣呢?下面我們采用官網(wǎng)的示例在相同的環(huán)境和設(shè)備下做下對比。

啟動了 1000個協(xié)程,并且為每個協(xié)程都輸出一個點

Coroutine

  var startTime = System.currentTimeMillis()
            repeat(times) { i -> // 啟動大量的協(xié)程
                GlobalScope.launch(Dispatchers.IO) {
                    Log.d(this@MainActivity.toString(), "$i=.")
                }

            }
            var endTime = System.currentTimeMillis() - startTime;
            Log.d(this@MainActivity.toString(), "endTime=$endTime")
            

執(zhí)行結(jié)果:endTime=239 ms

Thread

 var startTime = System.currentTimeMillis()
            repeat(times) { i ->// 啟動大量的線程
                Thread(Runnable {
                    Log.d(this@MainActivity.toString(), "$i=.")
                }).start()
            }
            var endTime = System.currentTimeMillis() - startTime;

執(zhí)行結(jié)果:endTime=3161 ms

Excutors

 var startTime = System.currentTimeMillis()
            var executors = Executors.newCachedThreadPool()
            repeat(times) { i -> // 使用線程池
                executors.execute {
                    Log.d(this@MainActivity.toString(), "$i=.")
                }
            }
            var endTime = System.currentTimeMillis() - startTime;
            Log.d(this@MainActivity.toString(), "endTime=$endTime")

執(zhí)行結(jié)果:endTime=143 ms

rxjava

      var startTime = System.currentTimeMillis()
            repeat(times) { i -> // 啟動Rxjava
                Observable.just("").subscribeOn(Schedulers.io())
                        .subscribe {
                            Log.d(this@MainActivity.toString(), "$i=.")
                        }
            }
            var endTime = System.currentTimeMillis() - startTime;
            Log.d(this@MainActivity.toString(), "endTime=$endTime")

執(zhí)行結(jié)果:endTime=241 ms

源碼工程:CorountineTest

Profiler

利用AS自帶的Profiler對運行時的CPU狀態(tài)進(jìn)行檢測,我們可以看到Thread對CPU的消耗比較大,Koroutine、Executor、RxJava的消耗基本差不多。

image

總結(jié)

從執(zhí)行時間和Profiler上看,Coroutine比使用Thread性能提升了一個量級,但與Excutor和RxJava性能是在一個量級上。

注意這里的例子為了簡便,因為異步執(zhí)行的時間基本和repeat的時間差不多,我們沒有等所有異步執(zhí)行完再打印時間,這里們不追求精確的時間,只為做量級上的對比。

實現(xiàn)機制

協(xié)程底層異步實現(xiàn)機制

我們先來看一段簡單的Kotlin程序。

GlobalScope.launch(Dispatchers.IO) {
            print("hello world")
        }

我們接著看下launch的實現(xiàn)代碼。

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

這里注意,我們通過追蹤最后的繼承關(guān)系發(fā)現(xiàn),DefaultScheduler.IO最后也是一個CoroutineContext。

接著發(fā)現(xiàn)繼續(xù)看coroutine.start的實現(xiàn),如下:

 public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) {
        initParentJob()
        start(block, receiver, this)
    }

接著繼續(xù)看CoroutineStart的start策略,如下:

  @InternalCoroutinesApi
    public operator fun <T> invoke(block: suspend () -> T, completion: Continuation<T>) =
        when (this) {
            CoroutineStart.DEFAULT -> block.startCoroutineCancellable(completion)
            CoroutineStart.ATOMIC -> block.startCoroutine(completion)
            CoroutineStart.UNDISPATCHED -> block.startCoroutineUndispatched(completion)
            CoroutineStart.LAZY -> Unit // will start lazily
        }

繼續(xù)看startCoroutineCancellable方法,如下:

@InternalCoroutinesApi
public fun <T> (suspend () -> T).startCoroutineCancellable(completion: Continuation<T>) = runSafely(completion) {
    createCoroutineUnintercepted(completion).intercepted().resumeCancellableWith(Result.success(Unit))
}

繼續(xù)看resumeCancellableWith方法實現(xiàn):

@InternalCoroutinesApi
public fun <T> Continuation<T>.resumeCancellableWith(result: Result<T>) = when (this) {
    is DispatchedContinuation -> resumeCancellableWith(result)
    else -> resumeWith(result)
}

最后發(fā)現(xiàn)調(diào)用的resumeCancellableWith方法實現(xiàn)如下:

   inline fun resumeCancellableWith(result: Result<T>) {
        val state = result.toState()
        if (dispatcher.isDispatchNeeded(context)) {
            _state = state
            resumeMode = MODE_CANCELLABLE
            dispatcher.dispatch(context, this)
        } else {
            executeUnconfined(state, MODE_CANCELLABLE) {
                if (!resumeCancelled()) {
                    resumeUndispatchedWith(result)
                }
            }
        }
    }

這里關(guān)鍵的觸發(fā)方法在這個地方

dispatcher.dispatch(context, this)

我們看 DefaultScheduler.IO最后的dispatch方法:

    override fun dispatch(context: CoroutineContext, block: Runnable): Unit =
        try {
            coroutineScheduler.dispatch(block)
        } catch (e: RejectedExecutionException) {
            DefaultExecutor.dispatch(context, block)
        }

這里我們最終發(fā)現(xiàn)是調(diào)用了CoroutineScheduler的dispatch方法,繼續(xù)看CoroutineScheduler的實現(xiàn)發(fā)現(xiàn),CoroutineScheduler繼承了Executor。

通過dispatch的調(diào)用最后可以發(fā)現(xiàn)CoroutineScheduler其實就是對Worker的調(diào)度,我們看看Worker的定義。

internal inner class Worker private constructor() : Thread()

通過這里我們發(fā)現(xiàn)另外一個老朋友Thread,所以到這里也符合上面性能驗證的測試結(jié)果。

到這里我們也有結(jié)論了,協(xié)程異步實現(xiàn)機制本質(zhì)也就是自定義的線程池。

非阻塞式掛起 suspend

suspend有什么作用,如何做到異步不用回調(diào)?下面先定義一個最簡單的suspend方法。

    suspend fun hello(){
        delay(100)
        print("hello world")
    }

通過Kotlin Bytecode轉(zhuǎn)換為java 代碼如下:

@Nullable
   public final Object hello(@NotNull Continuation $completion) {
      Object $continuation;
      label20: {
         if ($completion instanceof <undefinedtype>) {
            $continuation = (<undefinedtype>)$completion;
            if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) != 0) {
               ((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE;
               break label20;
            }
         }

         $continuation = new ContinuationImpl($completion) {
            // $FF: synthetic field
            Object result;
            int label;
            Object L$0;

            @Nullable
            public final Object invokeSuspend(@NotNull Object $result) {
               this.result = $result;
               this.label |= Integer.MIN_VALUE;
               return Test.this.hello(this);
            }
         };
      }

      Object $result = ((<undefinedtype>)$continuation).result;
      Object var6 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
      switch(((<undefinedtype>)$continuation).label) {
      case 0:
         ResultKt.throwOnFailure($result);
         ((<undefinedtype>)$continuation).L$0 = this;
         ((<undefinedtype>)$continuation).label = 1;
         if (DelayKt.delay(100L, (Continuation)$continuation) == var6) {
            return var6;
         }
         break;
      case 1:
         Test var7 = (Test)((<undefinedtype>)$continuation).L$0;
         ResultKt.throwOnFailure($result);
         break;
      default:
         throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
      }

      String var2 = "hello world";
      boolean var3 = false;
      System.out.print(var2);
      return Unit.INSTANCE;
   }

這里首先我們發(fā)現(xiàn)方法的參數(shù)多了一個Continuation completion并且內(nèi)部回定義一個 Object continuation,看看Continuation的定義。

@SinceKotlin("1.3")
public interface Continuation<in T> {
    /**
     * The context of the coroutine that corresponds to this continuation.
     */
    public val context: CoroutineContext

    /**
     * Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the
     * return value of the last suspension point.
     */
    public fun resumeWith(result: Result<T>)
}

這是一個回調(diào)接口,里面有一個關(guān)鍵的方法為resumeWith。 這個方法的具體調(diào)用通過上面的協(xié)程調(diào)用流程可以知道 ,在DispatchedContinuation的resumeCancellableWith會觸發(fā)。

public fun <T> Continuation<T>.resumeCancellableWith(result: Result<T>) = when (this) {
    is DispatchedContinuation -> resumeCancellableWith(result)
    else -> resumeWith(result)
}

那么resumeWith里面做了那些事情?我們看下具體的實現(xiàn)在ContinuationImpl的父類BaseContinuationImpl中。

 public final override fun resumeWith(result: Result<Any?>) {
        // This loop unrolls recursion in current.resumeWith(param) to make saner and shorter stack traces on resume
        var current = this
        var param = result
        while (true) {
            // Invoke "resume" debug probe on every resumed continuation, so that a debugging library infrastructure
            // can precisely track what part of suspended callstack was already resumed
            probeCoroutineResumed(current)
            with(current) {
                val completion = completion!! // fail fast when trying to resume continuation without completion
                val outcome: Result<Any?> =
                    try {
                        val outcome = invokeSuspend(param)
                        if (outcome === COROUTINE_SUSPENDED) return
                        Result.success(outcome)
                    } catch (exception: Throwable) {
                        Result.failure(exception)
                    }
                releaseIntercepted() // this state machine instance is terminating
                if (completion is BaseContinuationImpl) {
                    // unrolling recursion via loop
                    current = completion
                    param = outcome
                } else {
                    // top-level completion reached -- invoke and return
                    completion.resumeWith(outcome)
                    return
                }
            }
        }
    }

首先我們發(fā)現(xiàn)這里其實是一個遞歸的循環(huán),并且會調(diào)用invokeSuspend方法觸發(fā)實際的調(diào)用,等待返回結(jié)果。通過上面的分析可以看出2點。

  1. 非阻塞是因為本身啟動一個協(xié)程也是使用線程池異步執(zhí)行,所以不會阻塞
  2. 協(xié)程并不是沒有回調(diào),而是將回調(diào)的接口(Continuation)及調(diào)度代碼在編譯器生成,不用自己編寫。
  3. resumeWith是一個循環(huán)及遞歸,所以會將協(xié)程內(nèi)定義的表達(dá)式順序串聯(lián)調(diào)用。達(dá)到掛起及恢復(fù)的鏈?zhǔn)秸{(diào)用。

總結(jié)

  1. 協(xié)程到底能夠解決什么樣的問題?
  • 解決異步回調(diào)嵌套
  • 解決異步任務(wù)之間協(xié)作
  1. 協(xié)程和我們常用的Executor、RxJava有什么區(qū)別?
  • 從任務(wù)調(diào)度上看,本質(zhì)都是線程池的封裝
  1. 項目上使用有什么風(fēng)險嗎?
  • 從性能上看與線程池與RxJava在一個量級
  • 目前已是穩(wěn)定版本1.3.3,開源項目使用多
  • 代碼使用簡便,可維護(hù)性高
  • 開源生態(tài)支持良好,方便使用(Retrofit、Jitpack已支持)
  • 團隊學(xué)習(xí)及舊項目改造需要投入一定成本

參考資料

www.kotlincn.net

關(guān)于

歡迎關(guān)注我的個人公眾號

微信搜索:一碼一浮生,或者搜索公眾號ID:life2code

image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,953評論 2 373