0%

runBlocking阻塞的实现原理

runBlocking 的阻塞实现原理是 Kotlin 协程基础架构中非常精妙的一部分。它并不是通过简单的“忙等待”(busy-wait)来实现阻塞,而是通过在当前线程上启动一个事件循环(Event Loop) 来实现的。

下面我们分层来解析它的实现原理。

核心原理:线程局部事件循环 (Thread-Local Event Loop)

当调用 runBlocking 时,它的核心工作是在当前线程上安装并运行一个事件循环。这个事件循环负责调度和执行其代码块内所有的协程任务,直到所有任务完成。

1. 阻塞的入口:runBlocking 调用

当你调用 runBlockBlocking { ... } 时,会发生以下几步:

a. 创建事件循环和调度器

  • runBlocking 会创建一个 BlockingEventLoop 作为当前线程的局部事件循环。

  • 同时,它会创建一个 BlockingCoroutine 实例,它使用这个新创建的事件循环作为其协程上下文中的 ContinuationInterceptor(调度器)。

b. 启动协程

  • 你传入的 lambda 代码块被包装成一个协程(StandaloneCoroutine),并在这个新创建的、使用阻塞事件循环的上下文中启动。

c. 进入事件循环

  • 这是最关键的一步。runBlocking 会调用 eventLoop.join() 或类似的机制(在旧版本中是 joinBlocking)。

  • 这个 join() 方法会启动一个 while 循环,这个循环会持续运行,直到满足两个条件:

    1. 事件循环中没有可立即执行的任务(协程)。

    2. 事件循环已经退出isCompleted 为 true),即所有子协程都已完成。

2. 事件循环的内部工作方式

这个 while 循环就是“阻塞”发生的地方,但它内部是高效的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 概念上的伪代码,简化自 kotlinx.coroutines.EventLoopImplBase
private fun joinBlocking() {
val eventLoop = ThreadLocalEventLoop.current() as BlockingEventLoop
while (true) {
// 1. 检查是否所有工作都已完成
if (eventLoop.isCompleted) break // 条件2满足,退出循环,解除阻塞

// 2. 处理所有队列中的任务
val task = eventLoop.processNextEvent()
if (task != null) {
// 执行任务(例如,恢复一个挂起的协程)
task.run()
} else {
// 3. 如果没有立即要处理的任务,但事件循环还未结束
// 就让线程真正地“睡眠”一小段时间,而不是忙等待
// 在JVM上,这可能会调用 LockSupport.parkNanos()
eventLoop.parkNanos()
}
}
}
  • 有任务时:循环会不断地从事件队列中取出任务(例如,一个 delay 定时器到期了,需要恢复某个协程)并执行它。此时线程是在“工作”的。

  • 无任务且未完成时:线程会通过 parkNanos 等方法被短暂挂起,避免 100% 的 CPU 占用。一旦有新任务被提交到事件循环(例如,一个定时器到期),线程会被唤醒并继续工作。

3. 如何结束阻塞?

当 runBlocking 代码块内部的所有协程(包括所有子协程)都执行完毕后,根协程的状态会变为“已完成”。这会触发事件循环将自己的 isCompleted 标志设置为 true

外层的 while 循环检测到这个条件后,就会 break 退出循环。runBlocking 方法随之返回,调用线程得以继续执行后面的代码。阻塞解除

与挂起 (Suspend) 的关键区别

为了更好理解,我们对比一下 runBlocking 和普通协程构建器(如 launch)的区别:

特性 runBlocking launch / async
线程行为 阻塞 (Block) 挂起 (Suspend)
实现机制 在当前线程启动一个事件循环并运行 while 循环。 将协程体包装成状态机,通过 Continuation.resumeWith() 在调度器线程池中执行。
资源占用 占用调用线程,但通过事件循环高效调度,无任务时线程会短暂休眠。 不占用调用线程。当协程挂起时,底层线程立即被释放,可去执行其他任务。
使用场景 主函数、测试、集成阻塞代码与协程世界。 常规的异步、并发编程。
  • 挂起:是协程的行为。一个挂起函数释放了它当前占用的线程,当结果准备好时,它会在另一个线程(由调度器决定)上恢复执行。

  • 阻塞:是线程的行为。一个阻塞操作占用了线程,在操作完成之前,这个线程不能做任何其他事情。runBlocking 的“阻塞”是阻塞在它自己的事件循环上。

总结:runBlocking 的阻塞原理

  1. 安装事件循环runBlocking 在当前线程上设置了一个专属于它的 **BlockingEventLoop**。

  2. 运行循环至完成:它启动一个 while 循环,这个循环会持续运行,处理事件循环中的任务(恢复协程、执行定时操作等)。

  3. 高效等待:循环在没有立即任务但协程未全部完成时,会通过 park 等操作让线程短暂休眠,避免CPU空转。

  4. 退出条件:一旦它内部的所有协程工作完成,循环退出,runBlocking 返回,线程解除阻塞。

所以,runBlocking 的阻塞是一种有生产力的阻塞——线程虽然被“卡”在了一个循环里,但这个循环正在高效地驱动着整个协程世界的运转。这正是它不能用在已有协程环境中的原因,因为你会把一个本该用于处理大量协程的线程(如 Dispatchers.IO 中的线程)浪费在运行这一个事件循环上。

由Deepseek生成,已理解。