记录一次 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")
            }
        }
相关推荐
Yawesh_best18 分钟前
MySQL(5)【数据类型 —— 字符串类型】
android·mysql·adb
曾经的三心草3 小时前
Mysql之约束与事件
android·数据库·mysql·事件·约束
guoruijun_2012_47 小时前
fastadmin多个表crud连表操作步骤
android·java·开发语言
Winston Wood7 小时前
一文了解Android中的AudioFlinger
android·音频
B.-8 小时前
Flutter 应用在真机上调试的流程
android·flutter·ios·xcode·android-studio
有趣的杰克8 小时前
Flutter【04】高性能表单架构设计
android·flutter·dart
大耳猫14 小时前
主动测量View的宽高
android·ui
帅次16 小时前
Android CoordinatorLayout:打造高效交互界面的利器
android·gradle·android studio·rxjava·android jetpack·androidx·appcompat
枯骨成佛17 小时前
Android中Crash Debug技巧
android
kim56591 天前
android studio 更改gradle版本方法(备忘)
android·ide·gradle·android studio