八、runBlocking入门

launch 启动无返回值的协程,async 启动有返回值的协程——而 runBlocking 是第三种:它把协程代码封装成阻塞式的,自带 CoroutineContext,在当前线程上运行事件循环直到完成

runBlocking 协程启动 阻塞 CoroutineScope CoroutineContext

目录导航(点击跳转)

一、协程的三种启动方式:launch、async 与 runBlocking

1launch 与 async 的职责分工

在协程世界中,最常用的两个协程构建器各有明确的分工:

launch
启动一个没有返回值的协程。返回一个 Job,用于管理协程的生命周期(取消、等待完成)。典型场景:执行副作用操作,如写日志、更新 UI、发送埋点。
async
启动一个有返回值的协程。返回一个 Deferred(Job 的子接口),需要通过 await() 获取结果。典型场景:并行计算多个结果然后汇总。

两者都是在某个 CoroutineScope 的上下文中启动的:

// launch:启动后不关心返回值,只管"做"
scope.launch {
    doSomething()          // 副作用操作
    println("done")
}

// async:启动后需要用 await() 取回结果
val deferred: Deferred<String> = scope.async {
    fetchData()            // 返回计算结果
}
val result = deferred.await()  // 拿到结果

那么问题来了:如果当前没有 CoroutineScope,怎么办?比如在 JUnit 测试方法里、在 JVM main 函数里——这些入口天然不是协程世界的一部分。

2第三种方式:runBlocking

runBlocking 就是为这个边界场景设计的。它自己就是入口,不需要外部传入 CoroutineScope:

// runBlocking 不需要 scope,自己就是根
runBlocking {
    // 在内部,一切协程能力正常使用
    launch { delay(1000); println("子协程1完成") }
    launch { delay(2000); println("子协程2完成") }
    println("所有子协程已启动")
}
println("runBlocking 已返回 —— 此时所有子协程都已执行完毕")

三种启动方式的定位对比如下:

特性launchasyncrunBlocking
返回值Job(无结果值)Deferred(需 await)协程体的返回值
是否需要 CoroutineScope需要需要不需要,自带
阻塞当前线程
典型场景副作用操作并行计算桥接同步/异步边界
日常协程代码中使用频繁频繁极少(仅边界)
一句话区分:launch 是"去做吧",async 是"去算吧,我回头取结果",runBlocking 是"就在这里,等着,做完再走"。

二、runBlocking 为什么不需要 CoroutineScope?

1CoroutineScope 提供了什么?

要理解 runBlocking 为什么不需要 CoroutineScope,先要搞清楚 CoroutineScope 到底提供了什么:

1
CoroutineScope
提供协程运行的上下文容器,核心属性是 coroutineContext
2
CoroutineContext
协程的配置集合,包含 Job、Dispatcher、Interceptor 等
3
ContinuationInterceptor
拦截器决定协程运行在哪个线程上

展开来看:

  • CoroutineContext(协程上下文):一套键值对集合,定义了协程的"运行环境"。里面包含 Job(生命周期)、CoroutineDispatcher(调度到哪个线程)、CoroutineInterceptor(拦截续体派发)等。
  • ContinuationInterceptor(续体拦截器):CoroutineContext 中最关键的组件之一。协程在挂起恢复时,通过它来决定"下一步代码跑在哪个线程上"。这就是为什么协程知道自己应该运行在什么线程上。
  • 取消能力:CoroutineScope 还提供对内部所有协程的统一取消——调用 scope.cancel() 就能取消整棵协程树。
// CoroutineScope 的核心就是一个 CoroutineContext
public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

// 扩展函数 launch 从 scope 中取出 context 来启动协程
public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}
关键点:launch 和 async 是 CoroutineScope 的扩展函数。它们必须依附于一个 scope,因为需要从 scope 中取出 CoroutineContext 来决定协程的运行线程、Job 层级等。
2runBlocking 自带了这一切

runBlocking 是一个顶层函数,不是扩展函数。它内部自己构造了一个专用的 CoroutineContext:

// runBlocking 是顶层函数,自己构建一切
public fun <T> runBlocking(
    context: CoroutineContext = EmptyCoroutineContext,
    block: suspend CoroutineScope.() -> T
): T {
    val currentThread = Thread.currentThread()
    // 创建绑定当前线程的专用拦截器
    val contextInterceptor = context[ContinuationInterceptor]
    val eventLoop: EventLoop?
    val newContext: CoroutineContext

    // 关键:创建 BlockingEventLoop,绑定到当前线程
    if (contextInterceptor == null) {
        eventLoop = ThreadLocalEventLoop.eventLoop
        newContext = GlobalScope.newCoroutineContext(context + eventLoop)
    } else {
        eventLoop = (contextInterceptor as? EventLoop)?.takeIf { it.shouldBeProcessedFromContext() }
        newContext = GlobalScope.newCoroutineContext(context)
    }

    val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
    coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
    return coroutine.joinBlocking()  // 阻塞当前线程,处理事件循环,直到完成
}

runBlocking 自带一个绑定当前线程的专用 CoroutineContext,它包含:

BlockingCoroutine
特殊的 Job 实现,绑定到调用 runBlocking 的线程。它的 joinBlocking() 方法会阻塞当前线程并运行事件循环。
BlockingEventLoop
在当前线程上排队处理协程事件(如 delay 到期后的恢复)。这是"阻塞但不死等"的关键。
内部 CoroutineScope
block 参数的接收者就是一个 CoroutineScope,所以内部可以正常使用 launch、async 等扩展函数。
核心理解:runBlocking 不是在"借用"一个外部的 scope,而是自己创造了一个完整的协程运行环境。这也是为什么它不需要你传入 CoroutineScope——它自己就是根。

三、runBlocking 的内部机制:绑定当前线程的事件循环

1阻塞 ≠ 死等:BlockingEventLoop 的工作原理

很多人听到"阻塞"就想到死循环、浪费 CPU。但 runBlocking 的阻塞是有意义的:它阻塞当前线程,同时在该线程上运行协程的事件循环,直到协程执行完毕或被取消后才释放线程。

用下面这个例子直观感受 runBlocking 的事件循环:

fun main() {
    println("main 线程: ${Thread.currentThread().name}")

    runBlocking {
        println("runBlocking 内部,线程: ${Thread.currentThread().name}")

        // 启动两个子协程(在同一个线程上并发)
        launch {
            println("[子协程1] 开始,线程: ${Thread.currentThread().name}")
            delay(1000)
            println("[子协程1] 完成")
        }
        launch {
            println("[子协程2] 开始,线程: ${Thread.currentThread().name}")
            delay(500)
            println("[子协程2] 完成")
        }

        println("所有子协程已启动,等待完成...")
    }

    println("runBlocking 已返回")
}

// 输出(注意线程名始终相同):
// main 线程: main
// runBlocking 内部,线程: main
// 所有子协程已启动,等待完成...
// [子协程1] 开始,线程: main
// [子协程2] 开始,线程: main
// [子协程2] 完成
// [子协程1] 完成
// runBlocking 已返回

用流程图展示 runBlocking 内部的执行流程:

调用线程(main)
进入 runBlocking
执行协程体(启动子协程)
进入 joinBlocking:循环处理事件
所有协程完成 → 释放线程
BlockingEventLoop
子协程1 delay(1000):注册 1000ms 后唤醒
子协程2 delay(500):注册 500ms 后唤醒
500ms 后唤醒子协程2 → 完成
1000ms 后唤醒子协程1 → 完成
这就是"阻塞 + 事件循环"的妙处:runBlocking 没有创建新线程,而是在当前线程上处理各个协程的挂起与恢复。它把协程格式的代码封装起来,变成阻塞式的,并且安全且符合结构化并发的规范。
2取消机制:线程中断与内部异常

runBlocking 支持两种取消路径:

取消路径触发方式内部映射
外部中断Thread.interrupt()自动转换为 CancellationException,触发结构化取消
内部异常子协程抛出未捕获异常异常向上传播,取消所有子协程,runBlocking 提前返回并重新抛出异常
// 取消路径1:外部中断
val thread = thread {
    runBlocking {
        try {
            delay(Long.MAX_VALUE)
        } catch (e: CancellationException) {
            // Thread.interrupt() 被正确映射为 CancellationException
            println("被中断,资源已清理")
        }
    }
}
thread.interrupt()  // 外部触发取消

// 取消路径2:内部异常
runBlocking {
    launch {
        delay(500)
        throw RuntimeException("子协程出错了!")
    }
    launch {
        delay(5000)  // 这条日志永远不会执行
        println("不会打印")
    }
}
// runBlocking 在 ~500ms 后返回,并抛出 RuntimeException
与 launch/async 的区别:runBlocking 内部的异常会重新抛出给调用方(因为调用方是同步代码,只能通过异常来感知失败)。而 launch 的异常通过 CoroutineExceptionHandler 处理,async 的异常封装在 Deferred 中由 await 重新抛出。
3与 launch/async 的整体对比

将三者的启动机制放在一起对比,差异就很清晰了:

特性launchasyncrunBlocking
函数类型CoroutineScope 扩展函数CoroutineScope 扩展函数顶层函数
CoroutineContext 来源取自 scope + 参数合并取自 scope + 参数合并自建 BlockingCoroutine + EventLoop
返回值JobDeferred协程体返回的 T
线程行为非阻塞,挂起时释放线程非阻塞,挂起时释放线程阻塞当前线程,在事件循环中处理挂起恢复
异常处理CoroutineExceptionHandlerDeferred 中,await 时重新抛出直接重新抛出给调用方
是否需要 scope

深入内容请参阅本系列的深度解析篇:runBlocking:阻塞边界与设计目的——其中详细讨论了等待时间的 max vs sum 语义、调度器嵌套死锁风险、以及 Thread.interrupt() 到 CancellationException 的完整映射机制。

四、核心思想总结

核心要点

一句话总结

runBlocking 是协程世界的"入口适配器"——它自建 CoroutineContext,绑定当前线程的事件循环,把异步的协程代码封装成同步的阻塞调用;launch 和 async 依赖外部的 CoroutineScope 来提供这一切,而 runBlocking 自己就是根。