记载一次 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.Main
,Dispatchers.IO
和 Dispatcher.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.eventLoop
的 CoroutineContext
,它也是一个 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 的子协程,它指定的 ContinuationInterceptor
是 Dispatchers.Unconfined
, Dispatchers.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
前面。
那么为什么 2
和 3
在 4
的前面呢?很简略因为他们在履行时指定了运用 Dispatchers.IO
和 Dispatchers.Default
,他们不会在默许的 BlockingEventLoop
履行,Dispatchers.IO
与 Dispatchers.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")
}
}