记录一次 Kotlin 协程执行先后的面试问题

记录一次 Kotlin 协程执行先后的面试问题

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

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() 方法的源码:

Kotlin 复制代码
@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

Kotlin 复制代码
    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 中调度,就直接在当前的线程执行。

Kotlin 复制代码
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() 代码:

Kotlin 复制代码
    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 处理的协程的代码就相当于如下的代码(忽略协程的创建):

Kotlin 复制代码
       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 是不是有一样的效果?是的。以下的代码也会得到一样的输出结果:

Kotlin 复制代码
       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 :

Kotlin 复制代码
       runBlocking(Executors.newFixedThreadPool(2).asCoroutineDispatcher()) {
            launch {
                println("1")
            }
            launch(Dispatchers.IO) {
                println("2")
            }
            launch(Dispatchers.Default) {
                println("3")
            }
            launch(Dispatchers.Unconfined) {
                println("4")
            }
        }
相关推荐
太空漫步112 小时前
android社畜模拟器
android
海绵宝宝_4 小时前
【HarmonyOS NEXT】获取正式应用签名证书的签名信息
android·前端·华为·harmonyos·鸿蒙·鸿蒙应用开发
凯文的内存6 小时前
android 定制mtp连接外设的设备名称
android·media·mtp·mtpserver
天若子6 小时前
Android今日头条的屏幕适配方案
android
林的快手8 小时前
伪类选择器
android·前端·css·chrome·ajax·html·json
望佑8 小时前
Tmp detached view should be removed from RecyclerView before it can be recycled
android
xvch10 小时前
Kotlin 2.1.0 入门教程(二十四)泛型、泛型约束、绝对非空类型、下划线运算符
android·kotlin
人民的石头14 小时前
Android系统开发 给system/app传包报错
android
yujunlong391914 小时前
android,flutter 混合开发,通信,传参
android·flutter·混合开发·enginegroup
rkmhr_sef14 小时前
万字详解 MySQL MGR 高可用集群搭建
android·mysql·adb