來,跟我一起擼Kotlin runBlocking/launch/join/async/delay 原理&使用

前言

協(xié)程系列文章:

之前一些列的文章重點(diǎn)在于分析協(xié)程本質(zhì)原理,了解了協(xié)程的內(nèi)核再來看其它衍生的知識就比較容易了。
接下來這邊文章著重分析協(xié)程框架提供的一些重要的函數(shù)原理,通過本篇文章,你將了解到:

  1. runBlocking 使用與原理
  2. launch 使用與原理
  3. join 使用與原理
  4. async/await 使用與原理
  5. delay 使用與原理

1. runBlocking 使用與原理

默認(rèn)分發(fā)器的runBlocking

使用

老規(guī)矩,先上Demo:

    fun testBlock() {
        println("before runBlocking thread:${Thread.currentThread()}")
        //①
        runBlocking {
            println("I'm runBlocking start thread:${Thread.currentThread()}")
            Thread.sleep(2000)
            println("I'm runBlocking end")
        }
        //②
        println("after runBlocking:${Thread.currentThread()}")
    }

runBlocking 開啟了一個新的協(xié)程,它的特點(diǎn)是:

協(xié)程執(zhí)行結(jié)束后才會執(zhí)行runBlocking 后的代碼。

也就是① 執(zhí)行結(jié)束后 ② 才會執(zhí)行。

image.png

可以看出,協(xié)程運(yùn)行在當(dāng)前線程,因此若是在協(xié)程里執(zhí)行了耗時函數(shù),那么協(xié)程之后的代碼只能等待,基于這個特性,runBlocking 經(jīng)常用于一些測試的場景。

runBlocking 可以定義返回值,比如返回一個字符串:

    fun testBlock2() {
        var name = runBlocking {
            "fish"
        }
        println("name $name")
    }

原理

    #Builders.kt
    public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {
        //當(dāng)前線程
        val currentThread = Thread.currentThread()
        //先看有沒有攔截器
        val contextInterceptor = context[ContinuationInterceptor]
        val eventLoop: EventLoop?
        val newContext: CoroutineContext
        //----------①
        if (contextInterceptor == null) {
            //不特別指定的話沒有攔截器,使用loop構(gòu)建Context
            eventLoop = ThreadLocalEventLoop.eventLoop
            newContext = GlobalScope.newCoroutineContext(context + eventLoop)
        } else {
            eventLoop = (contextInterceptor as? EventLoop)?.takeIf { it.shouldBeProcessedFromContext() }
                ?: ThreadLocalEventLoop.currentOrNull()
            newContext = GlobalScope.newCoroutineContext(context)
        }
        //BlockingCoroutine 顧名思義,阻塞的協(xié)程
        val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
        //開啟
        coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
        //等待協(xié)程執(zhí)行完成----------②
        return coroutine.joinBlocking()
    }

重點(diǎn)看①②。

先說①,因?yàn)槲覀儧]有指定分發(fā)器,因此會使用loop,實(shí)際創(chuàng)建的是BlockingEventLoop,它繼承自EventLoopImplBase,最終繼承自CoroutineDispatcher(注意此處是個重點(diǎn))。
根據(jù)我們之前分析的協(xié)程知識可知,協(xié)程啟動后會構(gòu)造DispatchedContinuation,然后依靠dispatcher將runnable 分發(fā)執(zhí)行,而這個dispatcher 即是BlockingEventLoop。

    #EventLoop.common.kt
    //重寫dispatch函數(shù)
    public final override fun dispatch(context: CoroutineContext, block: Runnable) = enqueue(block)

    public fun enqueue(task: Runnable) {
        //將task 加入隊(duì)列,task = DispatchedContinuation
        if (enqueueImpl(task)) {
            unpark()
        } else {
            DefaultExecutor.enqueue(task)
        }
    }

BlockingEventLoop 的父類EventLoopImplBase 里有個成員變量:_queue,它是個隊(duì)列,用來存儲提交的任務(wù)。

再看②:
協(xié)程任務(wù)已經(jīng)提交到隊(duì)列里,就看啥時候取出來執(zhí)行了。

#Builders.kt
    fun joinBlocking(): T {
        try {
            try {
                while (true) {
                    //當(dāng)前線程已經(jīng)中斷了,直接退出
                    if (Thread.interrupted()) throw InterruptedException().also { cancelCoroutine(it) }
                    //如果eventLoop!= null,則從隊(duì)列里取出task并執(zhí)行
                    val parkNanos = eventLoop?.processNextEvent() ?: Long.MAX_VALUE
                    //協(xié)程執(zhí)行結(jié)束,跳出循環(huán)
                    if (isCompleted) break
                    //掛起線程,parkNanos 指的是掛起時間
                    parkNanos(this, parkNanos)
                    //當(dāng)線程被喚醒后,繼續(xù)while循環(huán)
                }
            } finally { // paranoia
            }
        }
        //返回結(jié)果
        return state as T
    }

#EventLoop.common.kt
    override fun processNextEvent(): Long {
        //延遲隊(duì)列
        val delayed = _delayed.value
        //延遲隊(duì)列處理,這里在分析delay時再解釋
        //從隊(duì)列里取出task
        val task = dequeue()
        if (task != null) {
            //執(zhí)行task
            task.run()
            return 0
        }
        return nextTime
    }

上面代碼的任務(wù)有兩個:

  1. 嘗試從隊(duì)列里取出Task。
  2. 若是沒有則掛起線程。

結(jié)合①②兩點(diǎn),再來過一下場景:

  1. 先創(chuàng)建協(xié)程,包裝為DispatchedContinuation,作為task。
  2. 分發(fā)task,將task加入到隊(duì)列里。
  3. 從隊(duì)列里取出task執(zhí)行,實(shí)際執(zhí)行的即是協(xié)程體。
  4. 當(dāng)3執(zhí)行完畢后,runBlocking()函數(shù)也就退出了。
image.png

其中虛線箭頭表示執(zhí)行先后順序。
由此可見,runBlocking()函數(shù)需要等待協(xié)程執(zhí)行完畢后才退出。

指定分發(fā)器的runBlocking

上個Demo在使用runBlocking 時沒有指定其分發(fā)器,若是指定了又是怎么樣的流程呢?

    fun testBlock3() {
        println("before runBlocking thread:${Thread.currentThread()}")
        //①
        runBlocking(Dispatchers.IO) {
            println("I'm runBlocking start thread:${Thread.currentThread()}")
            Thread.sleep(2000)
            println("I'm runBlocking end")
        }
        //②
        println("after runBlocking:${Thread.currentThread()}")
    }

指定在子線程里進(jìn)行分發(fā)。
此處與默認(rèn)分發(fā)器最大的差別在于:

默認(rèn)分發(fā)器加入隊(duì)列、取出隊(duì)列都是同一個線程,而指定分發(fā)器后task不會加入到隊(duì)列里,task的調(diào)度執(zhí)行完全由指定的分發(fā)器完成。

也就是說,coroutine.joinBlocking()后,當(dāng)前線程一定會被掛起。等到協(xié)程執(zhí)行完畢后再喚醒當(dāng)前被掛起的線程。
喚醒之處在于:

#Builders.kt
    override fun afterCompletion(state: Any?) {
        // wake up blocked thread
        if (Thread.currentThread() != blockedThread)
            //blockedThread 即為調(diào)用coroutine.joinBlocking()后阻塞的線程
            //Thread.currentThread() 為線程池的線程
            //喚醒線程
            unpark(blockedThread)
    }
image.png

紅色部分比紫色部分先執(zhí)行,因此紅色部分執(zhí)行的線程會阻塞,等待紫色部分執(zhí)行完畢后將它喚醒,最后runBlocking()函數(shù)執(zhí)行結(jié)束了。

不管是否指定分發(fā)器,runBlocking() 都會阻塞等待協(xié)程執(zhí)行完畢。

2. launch 使用與原理

想必大家剛接觸協(xié)程的時候使用最多的還是launch啟動協(xié)程吧。
看個Demo:

    fun testLaunch() {
        var job = GlobalScope.launch {
            println("hello job1 start")//①
            Thread.sleep(2000)
            println("hello job1 end")//②
        }
        println("continue...")//③
    }

非常簡單,啟動一個線程,打印結(jié)果如下:


image.png

③一定比①②先打印,同時也說明launch()函數(shù)并不阻塞當(dāng)前線程。
關(guān)于協(xié)程原理,在之前的文章都有深入分析,此處不再贅述,以圖示之:


image.png

3. join 使用與原理

雖然launch()函數(shù)不阻塞線程,但是我們就想要知道協(xié)程執(zhí)行完畢沒,進(jìn)而根據(jù)結(jié)果確定是否繼續(xù)往下執(zhí)行,這時候該Job.join()出場了。
先看該函數(shù)的定義:

#Job.kt
public suspend fun join()

是個suspend 修飾的函數(shù),suspend 是咱們的老朋友了,說明協(xié)程執(zhí)行到該函數(shù)會掛起(當(dāng)前線程不阻塞,另有他用)。
繼續(xù)看其實(shí)現(xiàn):

    #JobSupport.kt
    public final override suspend fun join() {
        //快速判斷狀態(tài),不耗時
        if (!joinInternal()) { // fast-path no wait
            coroutineContext.ensureActive()
            return // do not suspend
        }
        //掛起的地方
        return joinSuspend() // slow-path wait
    }

    //suspendCancellableCoroutine 典型的掛起操作
    //cont 是封裝后的協(xié)程
    private suspend fun joinSuspend() = suspendCancellableCoroutine<Unit> { cont ->
        //執(zhí)行完這就掛起
        //disposeOnCancellation 是將cont 記錄在當(dāng)前協(xié)程的state里,構(gòu)造為node
        cont.disposeOnCancellation(invokeOnCompletion(handler = ResumeOnCompletion(cont).asHandler))
    }

其中suspendCancellableCoroutine 是掛起的核心所在,關(guān)于掛起的詳細(xì)分析請移步:講真,Kotlin 協(xié)程的掛起沒那么神秘(原理篇)

joinSuspend()函數(shù)有2個作用:

  1. 將當(dāng)前協(xié)程體存儲到Job的state里(作為node)。
  2. 將當(dāng)前協(xié)程掛起。

什么時候恢復(fù)呢?當(dāng)然是協(xié)程執(zhí)行完成后。

#JobSupport.kt
private class ResumeOnCompletion(
    private val continuation: Continuation<Unit>
) : JobNode() {
    //continuation 為協(xié)程的包裝體,它里面有我們真正的協(xié)程體
    //之后重新進(jìn)行分發(fā)
    override fun invoke(cause: Throwable?) = continuation.resume(Unit)
}

當(dāng)協(xié)程執(zhí)行完畢,會例行檢查當(dāng)前的state是否有掛著需要執(zhí)行的node,剛好我們在joinSuspend()里放了node,于是找到該node,進(jìn)而找到之前的協(xié)程體再次進(jìn)行分發(fā)。根據(jù)協(xié)程狀態(tài)機(jī)的知識可知,這是第二次執(zhí)行協(xié)程體,因此肯定會執(zhí)行job.join()之后的代碼,于是乎看起來的效果就是:

job.join() 等待協(xié)程執(zhí)行完畢后才會往下執(zhí)行。

語言比較蒼白,來個圖:


image.png

注:此處省略了協(xié)程掛起等相關(guān)知識,如果對此有疑惑請閱讀之前的文章。

4. async/await 使用與原理

launch 有2點(diǎn)不足之處:協(xié)程執(zhí)行沒有返回值。
這點(diǎn)我們從它的定義很容易獲悉:


image.png

然而,在有些場景我們需要返回值,此時輪到async/await 出場了。

    fun testAsync() {
        runBlocking {
            //啟動協(xié)程
            var job = GlobalScope.async {
                println("job1 start")
                Thread.sleep(10000)
                //返回值
                "fish"
            }
            //等待協(xié)程執(zhí)行結(jié)束,并返回協(xié)程結(jié)果
            var result = job.await()
            println("result:$result")
        }
    }

運(yùn)行結(jié)果:


image.png

接著來看實(shí)現(xiàn)原理。

    public fun <T> CoroutineScope.async(
        context: CoroutineContext = EmptyCoroutineContext,
        start: CoroutineStart = CoroutineStart.DEFAULT,
        block: suspend CoroutineScope.() -> T
    ): Deferred<T> {
        val newContext = newCoroutineContext(context)
        //構(gòu)造DeferredCoroutine
        val coroutine = if (start.isLazy)
            LazyDeferredCoroutine(newContext, block) else
            DeferredCoroutine<T>(newContext, active = true)
        //coroutine == DeferredCoroutine
        coroutine.start(start, coroutine, block)
        return coroutine
    }

與launch 啟動方式不同的是,async 的協(xié)程定義了返回值,是個泛型。并且async里使用的是DeferredCoroutine,顧名思義:延遲給結(jié)果的協(xié)程。
后面的流程都是一樣的,不再細(xì)說。

再來看Job.await(),它與Job.join()類似:

  1. 先判斷是否需要掛起,若是協(xié)程已經(jīng)結(jié)束/被取消,當(dāng)然就無需等待直接返回。
  2. 先將當(dāng)前協(xié)程體包裝到state里作為node存放,然后掛起協(xié)程。
  3. 等待async里的協(xié)程執(zhí)行完畢,再重新調(diào)度執(zhí)行await()之后的代碼。
  4. 此時協(xié)程的值已經(jīng)返回。

這里需要重點(diǎn)關(guān)注一下返回值是怎么傳遞過來的。


image.png

將testAsync()反編譯:

    public final Object invokeSuspend(@NotNull Object $result) {
        //result 為協(xié)程執(zhí)行結(jié)果
        Object var6 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
        Object var10000;
        switch(this.label) {
            case 0:
                //第一次執(zhí)行這
                ResultKt.throwOnFailure($result);
                Deferred job = BuildersKt.async$default((CoroutineScope) GlobalScope.INSTANCE, (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
                    int label;
                    @Nullable
                    public final Object invokeSuspend(@NotNull Object var1) {
                        Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
                        switch(this.label) {
                            case 0:
                                ResultKt.throwOnFailure(var1);
                                String var2 = "job1 start";
                                boolean var3 = false;
                                System.out.println(var2);
                                Thread.sleep(10000L);
                                return "fish";
                            default:
                                throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
                        }
                    }
                }), 3, (Object)null);
                this.label = 1;
                //掛起
                var10000 = job.await(this);
                if (var10000 == var6) {
                    return var6;
                }
                break;
            case 1:
                //第二次執(zhí)行這
                ResultKt.throwOnFailure($result);
                //result 就是demo里的"fish"
                var10000 = $result;
                break;
            default:
                throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
        }

        String result = (String)var10000;
        String var4 = "result:" + result;
        boolean var5 = false;
        System.out.println(var4);
        return Unit.INSTANCE;
    }

很明顯,外層的協(xié)程(runBlocking)體會執(zhí)行2次。
第1次:調(diào)用invokeSuspend(xx),此時參數(shù)xx=Unit,后遇到await 被掛起。
第2次:子協(xié)程執(zhí)行結(jié)束并返回結(jié)果"fish",恢復(fù)外部協(xié)程時再次調(diào)用invokeSuspend(xx),此時參數(shù)xx="fish",并將參數(shù)保存下來,因此result 就有了值。

值得注意的是:
async 方式啟動的協(xié)程,若是協(xié)程發(fā)生了異常,不會像launch 那樣直接拋出,而是需要等待調(diào)用await()時拋出。

5. delay 使用與原理

線程可以被阻塞,協(xié)程可以被掛起,掛起后的協(xié)程等待時機(jī)成熟可以被恢復(fù)。

    fun testDelay() {
        GlobalScope.launch {
            println("before getName")
            var name = getUserName()
            println("after getName name:$name")
        }
    }
    suspend fun getUserName():String {
        return withContext(Dispatchers.IO) {
            //模擬網(wǎng)絡(luò)獲取
            Thread.sleep(2000)
            "fish"
        }
    }

獲取用戶名字是在子線程獲取的,它是個掛起函數(shù),當(dāng)協(xié)程執(zhí)行到此時掛起,等待獲取名字之后再恢復(fù)運(yùn)行。

有時候我們僅僅只是想要協(xié)程掛起一段時間,并不需要去做其它操作,這個時候我們可以選擇使用delay(xx)函數(shù):

    fun testDelay2() {
        GlobalScope.launch {
            println("before delay")
            //協(xié)程掛起5s
            delay(5000)
            println("after delay")
        }
    }

再來看看其原理。

#Delay.kt
    public suspend fun delay(timeMillis: Long) {
        //沒必要延時
        if (timeMillis <= 0) return // don't delay
        return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
            //封裝協(xié)程為cont,便于之后恢復(fù)
            if (timeMillis < Long.MAX_VALUE) {
                //核心實(shí)現(xiàn)
                cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
            }
        }
    }

主要看context.delay 實(shí)現(xiàn):

#DefaultExecutor.kt
internal actual val DefaultDelay: Delay = kotlinx.coroutines.DefaultExecutor

//單例
internal actual object DefaultExecutor : EventLoopImplBase(), Runnable {
    const val THREAD_NAME = "kotlinx.coroutines.DefaultExecutor"
    //...
    private fun createThreadSync(): Thread {
        return DefaultExecutor._thread ?: Thread(this, DefaultExecutor.THREAD_NAME).apply {
            DefaultExecutor._thread = this
            isDaemon = true
            start()
        }
    }
    //...
    override fun run() {
        //循環(huán)檢測隊(duì)列是否有內(nèi)容需要處理
        //決定是否要掛起線程
    }
    //...
}

DefaultExecutor 是個單例,它里邊開啟了線程,并且檢測隊(duì)列里任務(wù)的情況來決定是否需要掛起線程等待。

先看隊(duì)列的出入隊(duì)情況。

放入隊(duì)列
我們注意到DefaultExecutor 繼承自EventLoopImplBase(),在最開始分析runBlocking()時有提到過它里面有成員變量_queue 存儲隊(duì)列元素,實(shí)際上它還有另一個成員變量_delayed:

#EventLoop.common.kt
internal abstract class EventLoopImplBase: EventLoopImplPlatform(), Delay {
    //存放正常task
    private val _queue = atomic<Any?>(null)
    //存放延遲task
    private val _delayed = atomic<EventLoopImplBase.DelayedTaskQueue?>(null)
}

private inner class DelayedResumeTask(
    nanoTime: Long,
    private val cont: CancellableContinuation<Unit>
) : EventLoopImplBase.DelayedTask(nanoTime) {
    //協(xié)程恢復(fù)
    override fun run() { with(cont) { resumeUndispatched(Unit) } }
    override fun toString(): String = super.toString() + cont.toString()
}

delay.scheduleResumeAfterDelay 本質(zhì)是創(chuàng)建task:DelayedResumeTask,并將該task加入到延遲隊(duì)列_delayed里。

從隊(duì)列取出
DefaultExecutor 一開始就會調(diào)用processNextEvent()函數(shù)檢測隊(duì)列是否有數(shù)據(jù),如果沒有則將線程掛起一段時間(由processNextEvent()返回值確定)。
那么重點(diǎn)轉(zhuǎn)移到processNextEvent()上。

##EventLoop.common.kt
    override fun processNextEvent(): Long {
        if (processUnconfinedEvent()) return 0
        val delayed = _delayed.value
        if (delayed != null && !delayed.isEmpty) {
            //調(diào)用delay 后會放入
            //查看延遲隊(duì)列是否有任務(wù)
            val now = nanoTime()
            while (true) {
                //一直取任務(wù),直到取不到(時間未到)
                delayed.removeFirstIf {
                    //延遲任務(wù)時間是否已經(jīng)到了
                    if (it.timeToExecute(now)) {
                        //將延遲任務(wù)從延遲隊(duì)列取出,并加入到正常隊(duì)列里
                        enqueueImpl(it)
                    } else
                        false
                } ?: break // quit loop when nothing more to remove or enqueueImpl returns false on "isComplete"
            }
        }
        // 從正常隊(duì)列里取出
        val task = dequeue()
        if (task != null) {
            //執(zhí)行
            task.run()
            return 0
        }
        //返回線程需要掛起的時間
        return nextTime
    }

而執(zhí)行任務(wù)最終就是執(zhí)行DelayedResumeTask.run()函數(shù),該函數(shù)里會對協(xié)程進(jìn)行恢復(fù)。

至此,delay 流程就比較清晰了:

  1. 構(gòu)造task 加入到延遲隊(duì)列里,此時協(xié)程掛起。
  2. 有個單獨(dú)的線程會檢測是否需要取出task并執(zhí)行,沒到時間的話就要掛起等待。
  3. 時間到了從延遲隊(duì)列里取出并放入正常的隊(duì)列,并從正常隊(duì)列里取出執(zhí)行。
  4. task 執(zhí)行的過程就是協(xié)程恢復(fù)的過程。

老規(guī)矩,上圖:


image.png

圖上虛線紫色框部分表明delay 執(zhí)行到此就結(jié)束了,協(xié)程掛起(不阻塞當(dāng)前線程),剩下的就交給單例的DefaultExecutor 調(diào)度,等待延遲的時間結(jié)束后通知協(xié)程恢復(fù)即可。

關(guān)于協(xié)程一些常用的函數(shù)分析到此就結(jié)束了,下篇開始我們一起探索協(xié)程通信(Channel/Flow 等)相關(guān)知識。
由于篇幅原因,省略了一些源碼的分析,若你對此有疑惑,可評論或私信小魚人。

本文基于Kotlin 1.5.3,文中完整Demo請點(diǎn)擊

您若喜歡,請點(diǎn)贊、關(guān)注、收藏,您的鼓勵是我前進(jìn)的動力

持續(xù)更新中,和我一起步步為營系統(tǒng)、深入學(xué)習(xí)Android/Kotlin

1、Android各種Context的前世今生
2、Android DecorView 必知必會
3、Window/WindowManager 不可不知之事
4、View Measure/Layout/Draw 真明白了
5、Android事件分發(fā)全套服務(wù)
6、Android invalidate/postInvalidate/requestLayout 徹底厘清
7、Android Window 如何確定大小/onMeasure()多次執(zhí)行原因
8、Android事件驅(qū)動Handler-Message-Looper解析
9、Android 鍵盤一招搞定
10、Android 各種坐標(biāo)徹底明了
11、Android Activity/Window/View 的background
12、Android Activity創(chuàng)建到View的顯示過
13、Android IPC 系列
14、Android 存儲系列
15、Java 并發(fā)系列不再疑惑
16、Java 線程池系列
17、Android Jetpack 前置基礎(chǔ)系列
18、Android Jetpack 易懂易學(xué)系列
19、Kotlin 輕松入門系列
20、Kotlin 協(xié)程系列全面解讀

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容