记载一次 Kotlin 协程履行先后的面试问题

前几天有同事问了我一个这样的问题,问下面的程序打印的次序是什么:

object KotlinMain {
    @JvmStatic
    fun main(args: Array<String>) {
        runBlocking {
            launch {
                println("1")
            }
            launch(Dispatchers.IO) {
                println("2")
            }
            launch(Dispatchers.Default) {
                println("3")
            }
            launch(Dispatchers.Unconfined) {
                println("4")
            }
        }
    }
}

这儿直接给出答案,绝大多数的情况下输出是 2 3 4 1,假如移除掉打印 2 3 的代码,那么终究的打印输出一定是 4 1。这个问题考察了被面试者关于 Kotlin 协程的了解,假如关于协程的基本作业原理还不了解的同学可以参阅一下我之前写过的一篇文章: Kotlin 协程源码阅读笔记 —— 协程作业原理
协程使命的调度都是经过 ContinuationInterceptor 来操控的,像我们平时常用的 Dispatchers.MainDispatchers.IODispatcher.Default 他们都是属于 ContinuationInterceptor。像遇到这种调用先后的问题首要要要点检查对应的 ContinuationInterceptor 的作业方式。

首要看看 runBlocking() 办法的源码

@Throws(InterruptedException::class)
public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    val currentThread = Thread.currentThread()
    val contextInterceptor = context[ContinuationInterceptor]
    val eventLoop: EventLoop?
    val newContext: CoroutineContext
    if (contextInterceptor == null) {
        // create or use private event loop if no dispatcher is specified
        eventLoop = ThreadLocalEventLoop.eventLoop
        newContext = GlobalScope.newCoroutineContext(context + eventLoop)
    } else {
       // ...
    }
    val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
    coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
    return coroutine.joinBlocking()
}

注意看上面的代码,在没有 ContinuationInterceptor 的时分会增加一个 ThreadLocalEventLoop.eventLoopCoroutineContext,它也是一个 ContinuationInterceptor。 我们去看看 eventLoop

    private val ref = CommonThreadLocal<EventLoop?>()
    internal val eventLoop: EventLoop
        get() = ref.get() ?: createEventLoop().also { ref.set(it) }
    internal actual fun createEventLoop(): EventLoop = BlockingEventLoop(Thread.currentThread())

上面的代码也很简略假如 EventLoop缓存为空,就创立一个新的 EventLoop,同时以当时线程作为构造函数的参数。

我在Kotlin 协程源码阅读笔记 —— 协程作业原理文章中有剖析过 coroutine.start() 的源码,它会为 runBlocking() 办法传入的 Lambda 目标狗造成一个 Continuation 目标,这个 Continuation 目标又会被 DispatchedContinuation 目标署理,然后履行 DispatchedContinuation#resumeWith() 办法来触发协程履行,经过 DispatchedContinuation 的处理,它的代码终究会在 EventLoop 的线程中履行。这儿要记住 EventLoop 中只要一个线程。
BlockingCoroutine#joinBlocking() 办法便是在等候协程履行完结,在履行完结之前假如当时线程需求等候是经过 LockSupport.parkNanos() 办法来暂停线程的,这个和 Java 中的 AQS 行列等候时相同。当协程履行完毕后就去拿到协程的 result 然后回来。具体的 joinBlocking() 的代码我就不剖析了。

首要履行协程时启动了一个子协程子协程中打印了 1,经过 launch() 办法启动子协程时假如没有指定新的 ContinuationInterceptor 那它就会复用父协程的,也便是说会运用 BlockingEventLoop 作为它的 ContinuationInterceptor,所以它履行时会向 BlockingEventLoop 增加一个使命,而 BlockingEventLoop 中只要一个线程,而目前正在履行 runBlocking() 中的 Lambda 中的代码,所以这时它是繁忙的,所以子协程中的使命会被增加到等候行列中。

我们先疏忽打印 2 和 3 的子协程,直接看看打印 4 的子协程,它指定的 ContinuationInterceptorDispatchers.UnconfinedDispatchers.Unconfined 非常特别,他表示不需求在 ContinuationInterceptor 中调度,就直接在当时的线程履行。

internal object Unconfined : CoroutineDispatcher() {
    override fun isDispatchNeeded(context: CoroutineContext): Boolean = false
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        // ...
    }
    override fun toString(): String = "Dispatchers.Unconfined"
}

Unconfined 便是经过 isDispatchNeeded() 办法回来 false 来完成它的这种不在 ContinuationInterceptor 中调度的特性的。

我们再简略看看 DispatchedContinuation 中的 resumeWith() 代码:

    override fun resumeWith(result: Result<T>) {
        val context = continuation.context
        val state = result.toState()
        if (dispatcher.isDispatchNeeded(context)) {
            // 经过 dispatcher 调度
            _state = state
            resumeMode = MODE_ATOMIC
            dispatcher.dispatch(context, this)
        } else {
            // 直接在当时线程履行
            executeUnconfined(state, MODE_ATOMIC) {
                withCoroutineContext(this.context, countOrElement) {
                    continuation.resumeWith(result)
                }
            }
        }
    }

经过 Dispatchers.Unconfined 处理的协程的代码就相当于如下的代码(疏忽协程的创立):

       runBlocking {
            launch {
                println("1")
            }
            println("4")
        }

因为默许 runBlocking() 是一个单线程的 ContinuationInterceptor,所以 println("1") 的代码需求等候 runBlocking() 中的 Lambda 办法履行完毕后才干履行,因为 println("4") 直接就会在 Lambda 中履行,所以打印的次序 4 一定在 1 前面。
那么为什么 234 的前面呢?很简略因为他们在履行时指定了运用 Dispatchers.IODispatchers.Default,他们不会在默许的 BlockingEventLoop 履行,Dispatchers.IODispatchers.Default 也是闲置的,不需求将使命加入到等候行列中,所以他们可以直接履行使命。所以输出是 2 3 4 1

这时分你或许就开窍了,println("1") 被推迟履行的原因是因为 runBlocking() 默许的 ContinuationInterceptor 中只要一个线程来处理使命,所以导致 println("1") 的使命等候,那么我用别的只要一个线程的 ContinuationInterceptor 是不是有相同的效果?是的。以下的代码也会得到相同的输出成果:

       runBlocking(Executors.newFixedThreadPool(1).asCoroutineDispatcher()) {
            launch {
                println("1")
            }
            launch(Dispatchers.IO) {
                println("2")
            }
            launch(Dispatchers.Default) {
                println("3")
            }
            launch(Dispatchers.Unconfined) {
                println("4")
            }
        }

而下面的代码的成果便是 1 2 3 4 :

       runBlocking(Executors.newFixedThreadPool(2).asCoroutineDispatcher()) {
            launch {
                println("1")
            }
            launch(Dispatchers.IO) {
                println("2")
            }
            launch(Dispatchers.Default) {
                println("3")
            }
            launch(Dispatchers.Unconfined) {
                println("4")
            }
        }