Kotlin原理runBlocking luanch join async delay

102 阅读5分钟

前言

默认分发器的runBlocking

fun testBlock() {
    log("testBlock")
    runBlocking {
        log("runBlocking")
        Thread.sleep(200)
        log("runBlocking end")
    }
    log("testBlocking end")
}

private fun log(msg: Any?) = println("[${Thread.currentThread()}] $msg")

输出:
[Thread[Test worker,5,main]] testBlock
[Thread[Test worker @coroutine#1,5,main]] runBlocking
[Thread[Test worker @coroutine#1,5,main]] runBlocking end
[Thread[Test worker,5,main]] testBlocking end

协程运行在当前线程,若在协程里执行了耗时函数,那么协程之后的代码只能等待。runBlocking常用于一些测试的场景(谁没事写个协程阻塞当前线程呢)。如果在主线程(UI)runBlocking使用Thread.sleep(10000)会在主线程触发ANR。但替换为协程的delay(10000)不会触发ANR。delay类似于Handler去post一个任务。

runBlocking是自定义返回值的:

fun main() {
    val name = runBlocking {
        "finish"
    }
    log("name:$name")
}
输出:
[Thread[Test worker,5,main]] name:finish
# Builders.kt
@Throws(InterruptedException::class)
public actual fun <T> runBlocking(context: CoroutineContext, 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
    // loop实际创建的是BlockingEventLoop,最终都是继承于CoroutineDispatcher。协程启动后会构造DispatchedContinuation,然后依靠dispatcher将runable分发执行,而这个dispatcher即是BlockingEventLoop
    if (contextInterceptor == null) {
        // 不特别指定的话没有拦截器,使用Loop构建Context
        eventLoop = ThreadLocalEventLoop.eventLoop
        newContext = GlobalScope.newCoroutineContext(context + eventLoop)
    } else {
        // See if context's interceptor is an event loop that we shall use (to support TestContext)
        // or take an existing thread-local event loop if present to avoid blocking it (but don't create one)
        eventLoop = (contextInterceptor as? EventLoop)?.takeIf { it.shouldBeProcessedFromContext() }
            ?: ThreadLocalEventLoop.currentOrNull()
        newContext = GlobalScope.newCoroutineContext(context)
    }
    // 阻塞的协程
    val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
    // 开启
    coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
    // 等待协程执行完成。协程任务已经提交到队列里,就看什么时候取出执行。
    return coroutine.joinBlocking()
}
# EventLoop.common.kt
public final override fun dispatch(context: CoroutineContext, block: Runnable) = enqueue(block)

open fun enqueue(task: Runnable) {
    // 将task加入队列,task=DispatchedContinuation
    if (enqueueImpl(task)) {
        // todo: we should unpark only when this delayed task became first in the queue
        unpark()
    } else {
        DefaultExecutor.enqueue(task)
    }
}

BlockingEventLoop的父类EventLoopImpIBase成员变量_queue(队列),存储提交的任务。

@Suppress("UNCHECKED_CAST")
fun joinBlocking(): T {
    registerTimeLoopThread()
    try {
        eventLoop?.incrementUseCount()
        try {
            while (true) {
                // 当前线程已经中断,直接退出
                if (Thread.interrupted()) throw InterruptedException().also { cancelCoroutine(it) }
                // 如果eventLoop!=null,从队列取出task执行
                val parkNanos = eventLoop?.processNextEvent() ?: Long.MAX_VALUE
                // 协程执行结束,跳出循环
                if (isCompleted) break
                // 挂起线程,parkNanos挂起时间
                parkNanos(this, parkNanos)
            }
        } finally { // paranoia
            eventLoop?.decrementUseCount()
        }
    } finally { // paranoia
        unregisterTimeLoopThread()
    }
    // now return result
    val state = this.state.unboxState()
    (state as? CompletedExceptionally)?.let { throw it.cause }
    // 返回结果
    return state as T
}

public open fun processNextEvent(): Long {
    if (!processUnconfinedEvent()) return Long.MAX_VALUE
    return 0
}

public fun processUnconfinedEvent(): Boolean {
    // 队列
    val queue = unconfinedQueue ?: return false
    // 从队列取出task
    val task = queue.removeFirstOrNull() ?: return false
    task.run()
    return true
}

尝试从队列里取出task,若没有则挂起线程。

  1. 先创建协程,包装为DispatchedContinuation,作为task
  2. 分发task,将task加入队列里
  3. 从队列里取出task执行,实际执行的即协程体
  4. 当执行完毕了,runBlocking函数也就退出了。

构造协程->包装为task->BlockingEventLoop分发->加入阻塞队列

从队列里取出task执行->执行协程体->runBlocking结束

指定分发器的runBlocking

fun main() {
    log("main")
    runBlocking(Dispatchers.IO) {
        log("runBlocking")
        Thread.sleep(2000)
        println("runBlocking end")
    }
    log("main end")
}
输出:
[Thread[Test worker,5,main]] main
[Thread[DefaultDispatcher-worker-1 @coroutine#1,5,main]] runBlocking
runBlocking end
[Thread[Test worker,5,main]] main end

指定在子线程里进行分发。

默认分发器加入队列、取出队列都是在同一个线程,而指定分发器后task不会加入到队列里,task的调度执行完全由指定的分发器完成。

coroutine.joinBlocking后,当前线程一定会被挂起。等到协程执行完毕后再唤醒当前被挂起的线程。唤醒处在于:

#Builders.kt
override fun afterCompletion(state: Any?) {
    // wake up blocked thread
    if (Thread.currentThread() != blockedThread)
        // blockedThread即为调用coroutine.joinBlocking后阻塞的线程
        // Thread.currentThread 为线程池的线程
        // 唤醒线程
        unpark(blockedThread)
}

构造协程->包装为task->线程池分发->子线程执行->线程池执行Task->执行协程体

执行协程体的过程中父线程阻塞等待->协程体执行结束 唤醒父线程->runBlocking结束

不管是否指定分发器,runBlocking都会阻塞等待协程执行完毕。

launch 使用与原理

fun main() {
    var job = GlobalScope.launch {
        log("launch")
        // 协程里不建议用Thread.sleep。建议用协程的delay
        Thread.sleep(200)
        log("launch end")
    }
    log("main end")
    // 防止父线程过早执行结束,导致子线程得不到执行
    Thread.sleep(300)
}
输出:
[Thread[Test worker,5,main]] main end
[Thread[DefaultDispatcher-worker-1 @coroutine#1,5,main]] launch
[Thread[DefaultDispatcher-worker-1 @coroutine#1,5,main]] launch end

launch并不阻塞当前线程。

构造协程->包装task->线程池分发->子线程执行->线程池执行task->执行协程体

join使用和原理

我们需要知道协程执行完毕没用Job.join。是挂起函数(当前线程不阻塞)

#Job.kt
public suspend fun join()

#JobSupport.kt
public final override suspend fun join() {
    // 快速判断状态不耗时
    if (!joinInternal()) { // fast-path no wait
        coroutineContext.ensureActive()
        return // do not suspend
    }
    // 挂起的地方
    return joinSuspend() // slow-path wait
}
// 封装后的协程。suspendCancellableCoroutine 典型的挂起操作
private suspend fun joinSuspend() = suspendCancellableCoroutine<Unit> { cont ->
    // 执行完就挂起
    cont.disposeOnCancellation(invokeOnCompletion(handler = ResumeOnCompletion(cont).asHandler))
}

suspendCancellableCoroutine是挂起的核心。

joinSuspend函数,将当前协程体存储到Job的state里(作为node)。将当前协程挂起。 协程执行完后恢复。

private class ResumeOnCompletion(
    private val continuation: Continuation<Unit>
) : JobNode() {
    // continuation 为协程的包装体,里面有我们真正的协程体。之后重新进行分发
    override fun invoke(cause: Throwable?) = continuation.resume(Unit)
}
fun main() {
    runBlocking {
        var job = GlobalScope.launch {
            log("launch")
            // 协程里不建议用Thread.sleep。建议用协程的delay
            Thread.sleep(200)
            log("launch end")
        }
        // 检测job没有执行完成;将协程体(runBlocking{}里的内容)放入job的state里;挂起协程;
        job.join()
        // job 完成后继续执行下面的代码
        log("main end")
        // 防止父线程过早执行结束,导致子线程得不到执行
        Thread.sleep(300)
    }
}
输出:
[Thread[DefaultDispatcher-worker-1 @coroutine#2,5,main]] launch
[Thread[DefaultDispatcher-worker-1 @coroutine#2,5,main]] launch end
[Thread[Test worker @coroutine#1,5,main]] main end

当协程执行完毕,会例行检查当前的state是否有挂着需要执行的node,刚好我们在joinSuspend里放了node,找到之前的协程体再次进行分发。根据协程状态机的知识可知,第二次执行协程体,因此肯定会执行job。join之后的代码。

job执行完成->寻找node重新分发执行(runBlocking内容)->根据协程状态,把剩余的runBlocking运行完成。

检测job没有执行完成->将协程体(runBlocking{}里的内容)放入job的state里。挂起协程

async/await使用和原理

launch协程执行没有返回值

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    // 返回值是Unit,没有返回值
    block: suspend CoroutineScope.() -> Unit
): Job 

用async/await带返回值。

fun main() {
    runBlocking {
        // 启动协程
        val job = GlobalScope.async {
            log("async")
            Thread.sleep(200)
            // 返回值
            "finish"
        }
        // 等待协程执行结束,并返回协程结果
        val result = job.await()
        log("result:$result")
    }
}
输出:
[Thread[DefaultDispatcher-worker-1 @coroutine#2,5,main]] async
[Thread[Test worker @coroutine#1,5,main]] result:finish
public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    // 定义泛型的返回值
    block: suspend CoroutineScope.() -> T
): Deferred<T> {
    val newContext = newCoroutineContext(context)
    // 构造DeferredCoroutine延迟给结果的协程
    val coroutine = if (start.isLazy)
        LazyDeferredCoroutine(newContext, block) else
        DeferredCoroutine<T>(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

先判断是否需要挂起,若线程已经结束/被取消,就无需等待直接返回。先将当前协程体包装到state里作为node存放,然后挂起协程。等待async里的写执行完成,再重新调度执行await之后的代码,此时协程的值已经返回。

经过分发,在子线程里执行invokeSuspend->实际执行的是协程体内容->因为await挂起了,在子协程结束时许哟啊恢复外部协程执行->执行runBlocking协程体->completion.resumeWith(outcome)此处的outCome="finish"->将结果存到result中.

外层runBlocking体会执行2次。

  1. 调用invokeSuspend(xx),此时参数xx=Unit,后遇到await被挂起
  2. 子协程(线程池重新创建一个线程)执行结束并返回结果"finish",恢复外部协程时再次调用invokeSuspend(xx),此时参数xx="finish",并保存参数,result就有了值。

async启动的协程,若协程发生了异常,不会像launch那样直接抛出,而是需要等待await时抛出。

delay使用与原理

线程可以被阻塞,协程可以被挂起,挂起后的协程等待机会成熟可以被恢复。

fun main() {
    GlobalScope.launch {
        log("launch")
        // 当协程执行到挂起函数这里,等待获取名字后再恢复运行
        var name = getUserName()
        log("name:$name")
    }
    // 防止launch协程得不到执行父线程就退出了
    Thread.sleep(2500)
}
// 挂起函数
private suspend fun getUserName(): String {
    return withContext(Dispatchers.IO) {
        // 模拟网络获取
        Thread.sleep(2000)
        "fish"
    }
}
输出:
[Thread[DefaultDispatcher-worker-1 @coroutine#1,5,main]] launch
[Thread[DefaultDispatcher-worker-2 @coroutine#1,5,main]] name:fish

协程挂起一段时间可用delay(xx)函数

fun main() {
    GlobalScope.launch {
        log("launch")
        delay(5000)
        log("launch end")
    }
    // 防止launch协程得不到执行父线程就退出了
    Thread.sleep(5100)
}
输出:
[Thread[DefaultDispatcher-worker-1 @coroutine#1,5,main]] launch
[Thread[DefaultDispatcher-worker-1 @coroutine#1,5,main]] launch end
public suspend fun delay(timeMillis: Long) {
    // 没必要延迟时
    if (timeMillis <= 0) return
    return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
        // if timeMillis == Long.MAX_VALUE then just wait forever like awaitCancellation, don't schedule.
        // 封装协程为cont,便于之后恢复
        if (timeMillis < Long.MAX_VALUE) {
            // 核心实现
            cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
        }
    }
}

#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() {
        //循环检测队列是否有内容需要处理
        //决定是否要挂起线程
    }
    //...
}

DefaultExecutor是单例,开启线程,检测队列里任务的情况来决定是否需要挂起线程等待。

放入队列:DefaultExecutor继承自EventLoopImplBase,在最开始分析runBlocking时它里面的_queue存储队列元素,成员变量_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) {
    //协程恢复
    override fun run() { with(cont) { resumeUndispatched(Unit) } }
    override fun toString(): String = super.toString() + cont.toString()
}

delay.scheduleResumeAfterDelay本质是创建task:DelayedResumeTask,并将该task加入到延迟队列_delayed里。

从队列取出:DefaultExecutor调用processNextEvent函数检测队列是否有数据,如果没有则将线程挂起一段时间(由processNextEvent返回值确定)。

##EventLoop.common.kt
    override fun processNextEvent(): Long {
        if (processUnconfinedEvent()) return 0
        val delayed = _delayed.value
        if (delayed != null && !delayed.isEmpty) {
            //调用delay 后会放入
            //查看延迟队列是否有任务
            val now = nanoTime()
            while (true) {
                //一直取任务,直到取不到(时间未到)
                delayed.removeFirstIf {
                    //延迟任务时间是否已经到了
                    if (it.timeToExecute(now)) {
                        //将延迟任务从延迟队列取出,并加入到正常队列里
                        enqueueImpl(it)
                    } else
                        false
                } ?: break // quit loop when nothing more to remove or enqueueImpl returns false on "isComplete"
            }
        }
        // 从正常队列里取出
        val task = dequeue()
        if (task != null) {
            //执行
            task.run()
            return 0
        }
        //返回线程需要挂起的时间
        return nextTime
    }

执行任务最终执行DelayedResumeTask.run函数,该函数对协程进行恢复。

  1. 构造task加入延迟队列,此时协程挂起
  2. 有个单独的线程会检测是否需要取出task并执行,没到时间就哟啊挂起等待
  3. 时间到了从延迟队列里取出并放入正常的队列,从正常队列取出并执行
  4. task执行的过程就是协程恢复的过程

delay单例开启一个线程->检测队列状态,决定线程是否挂起->挂起时间结束->检测延迟任务需要执行->延迟队列->从延迟队列取出,并加入正常队列->正常队列->从队列取出任务,任务核心:恢复执行协程

delay构造延迟任务,并加入延迟队列里->delay结束,协程挂起(不阻塞当前线程)