launch 启动无返回值的协程,async 启动有返回值的协程——而 runBlocking 是第三种:它把协程代码封装成阻塞式的,自带 CoroutineContext,在当前线程上运行事件循环直到完成
在协程世界中,最常用的两个协程构建器各有明确的分工:
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 函数里——这些入口天然不是协程世界的一部分。
runBlocking 就是为这个边界场景设计的。它自己就是入口,不需要外部传入 CoroutineScope:
// runBlocking 不需要 scope,自己就是根
runBlocking {
// 在内部,一切协程能力正常使用
launch { delay(1000); println("子协程1完成") }
launch { delay(2000); println("子协程2完成") }
println("所有子协程已启动")
}
println("runBlocking 已返回 —— 此时所有子协程都已执行完毕")
三种启动方式的定位对比如下:
| 特性 | launch | async | runBlocking |
|---|---|---|---|
| 返回值 | Job(无结果值) | Deferred(需 await) | 协程体的返回值 |
| 是否需要 CoroutineScope | 需要 | 需要 | 不需要,自带 |
| 阻塞当前线程 | 否 | 否 | 是 |
| 典型场景 | 副作用操作 | 并行计算 | 桥接同步/异步边界 |
| 日常协程代码中使用 | 频繁 | 频繁 | 极少(仅边界) |
要理解 runBlocking 为什么不需要 CoroutineScope,先要搞清楚 CoroutineScope 到底提供了什么:
展开来看:
// 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
}
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,它包含:
很多人听到"阻塞"就想到死循环、浪费 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 内部的执行流程:
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 |
|---|---|---|---|
| 函数类型 | CoroutineScope 扩展函数 | CoroutineScope 扩展函数 | 顶层函数 |
| CoroutineContext 来源 | 取自 scope + 参数合并 | 取自 scope + 参数合并 | 自建 BlockingCoroutine + EventLoop |
| 返回值 | Job | Deferred | 协程体返回的 T |
| 线程行为 | 非阻塞,挂起时释放线程 | 非阻塞,挂起时释放线程 | 阻塞当前线程,在事件循环中处理挂起恢复 |
| 异常处理 | CoroutineExceptionHandler | Deferred 中,await 时重新抛出 | 直接重新抛出给调用方 |
| 是否需要 scope | 是 | 是 | 否 |
深入内容请参阅本系列的深度解析篇:runBlocking:阻塞边界与设计目的——其中详细讨论了等待时间的 max vs sum 语义、调度器嵌套死锁风险、以及 Thread.interrupt() 到 CancellationException 的完整映射机制。
runBlocking 是协程世界的"入口适配器"——它自建 CoroutineContext,绑定当前线程的事件循环,把异步的协程代码封装成同步的阻塞调用;launch 和 async 依赖外部的 CoroutineScope 来提供这一切,而 runBlocking 自己就是根。