深入理解 Kotlin 协程:从零实现一个 IO 优先 + 虚拟线程溢出的混合调度器
前言:为什么我们需要混合调度器?
在 Kotlin 协程的世界里,Dispatchers.IO 是我们处理阻塞 IO 任务的标配。它基于线程池实现,默认并行度为 max(64, CPU 核数),足以应对大多数场景。
但随着 JDK 21 虚拟线程(Virtual Thread)的正式登场,一个问题摆在了我们面前:有没有办法既享受 IO 线程池的成熟稳定,又能在高峰时期利用虚拟线程的超高并发能力?
答案是:混合调度器(Hybrid Dispatcher)。
本文将带你从零实现一个生产级的混合调度器------Dispatchers.Hybrid,它的核心思想是:
IO 线程池优先,感知积压后溢出到虚拟线程,全路径饱和时支持五种降级策略。
在正式开始之前,先看一下它的调度流程图:
scss
dispatch(task)
│
├── IO 不拥挤? → Dispatchers.IO(快速路径)
│
├── IO 积压了? → 尝试虚拟线程溢出
│
└── VT 也满了? → 饱和策略
├─ SUSPEND 入队等待
├─ DROP 静默丢弃
├─ REJECT 抛出异常
├─ EXPAND 临时扩容 IO
└─ EXPAND_VT 临时扩容 VT
一、整体架构:三级调度模型
1.1 设计理念
混合调度器的设计遵循三个原则:
-
快速路径优先 :正常流量下走
Dispatchers.IO,零额外开销 -
弹性溢出:IO 路径拥挤时自动溢出到虚拟线程,不阻塞调用方
-
优雅降级:双路径都饱和时,按配置策略处理,而非简单抛异常
1.2 核心组件
| 组件 | 职责 |
|---|---|
HybridDispatcher |
调度器核心实现,维护所有状态与计数器 |
HybridConfig |
配置入口,支持运行时动态调参 |
ThreadLocalSnapshot |
VT 路径的 ThreadLocal 透传工具 |
OverflowStrategy |
饱和策略枚举 |
1.3 为什么不直接全用虚拟线程?
你可能会问:既然虚拟线程这么强,为什么不直接全部走虚拟线程?
原因有三:
-
ThreadLocal 兼容性:大量现有库(如 MDC、Spring 事务上下文)依赖 ThreadLocal,虚拟线程切换会丢失上下文
-
池化资源复用:数据库连接池、HTTP 连接池等资源与线程绑定,虚拟线程的海量创建可能击穿池化资源
-
成熟度考量 :
Dispatchers.IO经过多年生产验证,稳定性有保障,并且性能略优于虚拟线程
混合调度器的思路是:平时走 IO 保稳定,高峰用 VT 扛流量。
二、IO 拥挤感知:双计数器的精妙设计
2.1 问题:怎么知道 IO 线程池忙不忙?
Dispatchers.IO 是一个黑盒------我们无法直接查询它内部的队列长度。那怎么判断是否应该溢出到虚拟线程呢?
答案是:自己维护两个计数器,精确感知拥挤度。
kotlin
/** 已派发到 IO 路径、尚未完成的总数 */
private val ioDispatched = AtomicInteger(0)
/** 正在 IO 线程上实际执行的数量 */
private val ioExecuting = AtomicInteger(0)
这两个计数器的差值,就是 Dispatchers.IO 内部积压队列的近似长度:
ini
queued = ioDispatched - ioExecuting
当 queued >= queueThreshold 时,说明 IO 路径确实拥挤了,开始溢出到虚拟线程。
2.2 计数器的生命周期
两个计数器的增减时机非常关键:
-
ioDispatched:任务投递给 IO 时 +1,任务完成时 -1 -
ioExecuting:任务真正开始在 IO 线程上执行时 +1,执行结束时 -1
我们通过 IOWrapper 包装器来精确控制 ioExecuting:
kotlin
private inner class IOWrapper(private val delegate: Runnable) : Runnable {
override fun run() {
ioExecuting.incrementAndGet() // 真正开始执行
try {
delegate.run()
} finally {
ioExecuting.decrementAndGet()
onIOTaskComplete() // 完成回调
}
}
}
2.3 为什么不用 activeCoroutineCount?
你可能知道,协程调度器有 limitedParallelism() 等 API,但它们:
-
无法区分"排队中"和"执行中"
-
无法精确感知内部队列长度
-
与
Dispatchers.IO的交互不够透明
双计数器方案虽然简单,但胜在精确、可控、零依赖。
2.4 为什么选择 AtomicInteger 而不是 Semaphore?
你可能直觉会想到用 Semaphore 来做并发控制------设置 64 个许可,tryAcquire() 成功就执行,失败则走溢出。这在语义上完全成立,但在实现上会付出高昂的隐藏成本。
Semaphore 的底层是 AQS(AbstractQueuedSynchronizer)+ CLH 队列。在竞争激烈或快速路径上,tryAcquire 和 release 会走入 CAS 重试、CLH 节点入队甚至 park/unpark 系统调用,从用户态切到内核态。而我们的混合调度器核心诉求是:正常流量下 IO 路径要极轻、极快。
kotlin
// 旧: Semaphore(64+16) --- 底层 AQS + CAS + CLH 队列 + park/unpark
// 新: AtomicInteger --- 仅 volatile write (LOCK XADD),无锁队列
// 快速路径:单次 incrementAndGet
if (ioInFlight.incrementAndGet() <= effectiveIoLimit) {
ioDispatcher.dispatch(context, IOWrapper(block))
return
}
ioInFlight.decrementAndGet() // 回退
这里的核心差别在于:AtomicInteger 的 incrementAndGet 在 x86 上会被编译为单一的 LOCK XADD 指令,仅仅是一次带锁存器前缀的原子加法,不涉及任何队列维护、线程挂起或唤醒。只有当计数超出上限时,我们才执行一次轻量的回退并转入溢出逻辑,完全避免了 AQS 带来的开销。
换句话说,我们用 "乐观计数 + 快速回退" 取代了 "悲观许可 + 队列阻塞" 的重型机制。对于 IO 路径大多数情况下都会成功执行的场景,这种无锁化计数器的吞吐和延迟表现要显著优于 Semaphore。这也是整个混合调度器在稳态下能够逼近 Dispatchers.IO 原始性能的关键所在。
三、虚拟线程溢出:ThreadLocal 保鲜难题
3.1 虚拟线程的"坑"
虚拟线程虽然轻量,但有一个经典问题:ThreadLocal 无法自动跨线程传递。
当任务从 IO 线程溢出到虚拟线程时,原本在 IO 线程上的 MDC、事务上下文、用户会话等 ThreadLocal 变量会全部丢失,或者被其他虚拟线程污染。
这在生产环境中是不可接受的。
3.2 ThreadLocalSnapshot:快照式透传
我们的解决方案是 ThreadLocalSnapshot------在提交到虚拟线程之前,捕获已注册的 ThreadLocal 值,执行时恢复,执行完毕后清理。
kotlin
class ThreadLocalSnapshot private constructor(
private val entries: Array<out Pair<ThreadLocal<*>, Any?>>
) {
/** 从当前线程捕获已注册 ThreadLocal 的值 */
fun capture(): ThreadLocalSnapshot {
val newEntries = Array(entries.size) { i ->
val (tl, _) = entries[i]
tl to tl.get()
}
return ThreadLocalSnapshot(newEntries)
}
/** 恢复到当前线程 */
@Suppress("UNCHECKED_CAST")
fun apply() {
for ((tl, v) in entries) (tl as ThreadLocal<Any?>).set(v)
}
/** 清除当前线程的对应 ThreadLocal,防止泄露 */
fun clear() {
for ((tl, _) in entries) tl.remove()
}
}
3.3 使用方式
使用时需要先注册需要透传的 ThreadLocal:
kotlin
// 注册需要在 VT 路径透传的 ThreadLocal
Dispatchers.Hybrid.registerThreadLocal(MDC.getMDCAdapter().threadLocalMap)
Dispatchers.Hybrid.registerThreadLocal(MyContext.THREAD_LOCAL)
然后在 VT 任务的包装器中自动处理:
kotlin
private inner class VTWrapper(
private val delegate: Runnable,
private val tlSnapshot: ThreadLocalSnapshot,
) : Runnable {
override fun run() {
if (tlSnapshot !== ThreadLocalSnapshot.EMPTY) tlSnapshot.apply()
try {
delegate.run()
} finally {
if (tlSnapshot !== ThreadLocalSnapshot.EMPTY) tlSnapshot.clear()
if (vtLimit != null) vtInFlight.decrementAndGet()
}
}
}
3.4 设计细节:模板 + 捕获的两段式
注意 ThreadLocalSnapshot 的设计是两段式的:
-
模板(template):包含已注册的 ThreadLocal 列表,但值为 null
-
捕获(capture):基于模板,从当前线程读取实际值,生成新的快照
这样做的好处是:
-
注册表只维护一份模板,避免重复创建
-
每次捕获都是轻量级的数组操作
-
EMPTY单例优化了无注册场景的性能
四、饱和策略:五种降级方案
当 IO 路径和 VT 路径都饱和时,我们需要决定如何处理后续任务。OverflowStrategy 枚举定义了五种策略:
4.1 SUSPEND(默认):入队等待
kotlin
SUSPEND → pendingQueue.offer(task) // 入队,不阻塞线程
-
行为:任务进入等待队列,IO 任务完成后优先转交空位
-
适用场景:大多数业务场景,任务不能丢
-
优点:不丢失任务,不阻塞调用线程
-
注意:队列是无界的,极端情况下可能 OOM
4.2 DROP:静默丢弃
kotlin
DROP → droppedCount.incrementAndGet() // 只计数,不执行
-
行为:直接丢弃任务,仅增加丢弃计数
-
适用场景:日志、监控、埋点等可丢失的非关键任务
-
优点:零开销,保护系统不被压垮
-
注意:一定要有监控告警,否则丢了都不知道
4.3 REJECT:抛出异常
kotlin
REJECT → throw RejectedExecutionException(...)
-
行为 :抛出
RejectedExecutionException -
适用场景:需要明确失败信号的场景
-
优点:调用方可以明确感知并处理
-
注意:调用方必须有异常处理逻辑
4.4 EXPAND:临时扩容 IO
kotlin
EXPAND → ioCeiling += 1 // 提升 IO 上限后重试
-
行为 :临时提升
ioCeiling,重试 IO 路径 -
适用场景:IO 密集型突发流量,希望优先用平台线程
-
优点:不切换到 VT,保持 ThreadLocal 等上下文
-
注意 :有硬上限
expandHardLimit,触及后回退 SUSPEND
4.5 EXPAND_VT:临时扩容 VT
kotlin
EXPAND_VT → vtLimit += 1 // 提升 VT 上限后重试
-
行为 :临时提升
vtLimit,重试 VT 路径 -
适用场景:VT 设了上限,但希望有一定弹性
-
优点:虚拟线程开销小,扩容成本低
-
注意 :同样有硬上限
vtExpandHardLimit
五、核心源码解析
5.1 dispatch():热路径的三级判断
dispatch() 是调度器的核心入口,我们来逐行解析:
kotlin
override fun dispatch(context: CoroutineContext, block: Runnable) {
// 第一级:IO 硬上限检查
val ceiling = ioCeiling
if (ceiling > 0 && ioDispatched.get() >= ceiling) {
overflowOrEnqueue(context, block)
return
}
// 第二级:计算拥挤度
val dispatched = ioDispatched.incrementAndGet()
val executing = ioExecuting.get()
val queued = dispatched - executing
if (queued <= queueThreshold) {
// IO 健康:走快速路径
ioTaskCount.incrementAndGet()
ioDispatcher.dispatch(context, IOWrapper(block))
} else {
// IO 拥挤:回退计数,走溢出
ioDispatched.decrementAndGet()
overflowOrEnqueue(context, block)
}
}
这里有一个关键的先增后减技巧:
先
incrementAndGet()占住位置,判断后如果决定溢出,再decrementAndGet()归还。
这避免了 check-then-act 的竞态条件------虽然有轻微的性能损耗,但保证了正确性。
5.2 overflowOrEnqueue():溢出的统一入口
kotlin
private fun overflowOrEnqueue(context: CoroutineContext, block: Runnable) {
// 先试 VT
if (VT_AVAILABLE) {
val acquired = tryAcquireVT()
if (acquired) {
dispatchToVT(context, block)
return
}
// VT 满了 → EXPAND_VT 尝试扩容
if (config.overflowStrategy == OverflowStrategy.EXPAND_VT) {
val newLimit = (vtLimit ?: 0) + 1
if (newLimit <= config.vtExpandHardLimit) {
vtLimit = newLimit
if (tryAcquireVT()) {
dispatchToVT(context, block)
return
}
}
}
}
// 全路径饱和 → 按策略处理
when (config.overflowStrategy) {
OverflowStrategy.SUSPEND, OverflowStrategy.EXPAND_VT -> {
pendingQueue.offer(PendingTask(context, block))
pendingCount.incrementAndGet()
}
OverflowStrategy.DROP -> droppedCount.incrementAndGet()
OverflowStrategy.REJECT -> throw RejectedExecutionException(...)
OverflowStrategy.EXPAND -> {
ioCeiling += 1
if (ioCeiling <= config.expandHardLimit) {
if (ioDispatched.incrementAndGet() <= ioCeiling) {
ioTaskCount.incrementAndGet()
ioDispatcher.dispatch(context, IOWrapper(block))
return
}
ioDispatched.decrementAndGet()
}
pendingQueue.offer(PendingTask(context, block))
pendingCount.incrementAndGet()
}
}
}
5.3 onIOTaskComplete():空位转交的精妙设计
这是整个调度器最巧妙的部分------IO 任务完成时,优先把空位转交给等待队列中的任务,而不是直接归还计数。
kotlin
private fun onIOTaskComplete() {
// 第一优先级:从等待队列取一个任务,直接转交空位
val pending = pendingQueue.poll()
if (pending != null) {
pendingCount.decrementAndGet()
ioTaskCount.incrementAndGet()
// 注意:ioDispatched 计数不增减!
ioDispatcher.dispatch(pending.context, IOWrapper(pending.runnable))
return
}
// 没有等待任务:归还计数
ioDispatched.decrementAndGet()
// 二次检查:防止丢失唤醒
val retry = pendingQueue.poll()
if (retry != null) {
if (ioDispatched.incrementAndGet() <= ioCeiling) {
pendingCount.decrementAndGet()
ioTaskCount.incrementAndGet()
ioDispatcher.dispatch(retry.context, IOWrapper(retry.runnable))
return
}
// 已达上限,放回队列
pendingQueue.offer(retry)
ioDispatched.decrementAndGet()
}
}
为什么要二次检查?
因为在 poll() 和 decrementAndGet() 之间可能有新任务入队。如果不二次检查,会出现"有空位但队列里还有任务"的情况------这就是典型的**丢失唤醒(lost wakeup)**问题。
二次检查虽然增加了一点复杂度,但保证了在高并发下不会出现任务饿死。
六、使用
6.1 基础使用
最简单的用法,开箱即用:
kotlin
// 直接使用全局单例
withContext(Dispatchers.Hybrid) {
jdbcTemplate.query(...) // 阻塞 JDBC 调用
}
6.2 配置调优
kotlin
// 应用启动时配置一次即可
Dispatchers.Hybrid.apply {
ioCeiling = 128 // IO 路径并发上限
queueThreshold = 32 // 排队深度阈值
maxVirtualThreads = 5_000 // VT 上限
overflowStrategy = OverflowStrategy.DROP // 饱和策略
}
6.3 ThreadLocal 透传
kotlin
// 注册需要透传的 ThreadLocal
Dispatchers.Hybrid.registerThreadLocal(MDC.getCopyOfContextMap())
Dispatchers.Hybrid.registerThreadLocal(MyContext.THREAD_LOCAL)
6.4 自定义实例
kotlin
// 创建自定义配置的调度器(注意用完要 close)
val customDispatcher = Dispatchers.hybrid(
ioCeiling = 64,
queueThreshold = 16,
maxVirtualThreads = 10_000,
overflowStrategy = OverflowStrategy.EXPAND,
)
customDispatcher.use {
// 使用自定义调度器
}
6.5 监控与诊断
kotlin
// 打印当前状态快照
println(Dispatchers.Hybrid.snapshot())
// 输出示例:
// Hybrid[ioRunning=48, ioQueued=12, ioCeiling=64, qThreshold=16,
// vtInFlight=1234/unlimited, pending~=0, dropped=0, strategy=SUSPEND]
6.6 参数调优建议
| 参数 | 建议值 | 说明 |
|---|---|---|
ioCeiling |
64 ~ 256 | 根据 IO 类型调整,纯网络 IO 可以大一些 |
queueThreshold |
16 ~ 64 | 太小频繁切换 VT,太大响应延迟高 |
maxVirtualThreads |
1000 ~ 10000 | 根据下游系统承受能力设置 |
overflowStrategy |
SUSPEND / EXPAND | 核心业务用 SUSPEND,非核心用 DROP |
七、性能对比与适用场景
7.1 三种调度器对比
| 维度 | Dispatchers.IO | Dispatchers.Hybrid | 纯虚拟线程 |
|---|---|---|---|
| 低并发性能 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| 高并发吞吐 | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| ThreadLocal 兼容 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ |
| 资源可控性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ |
| 稳定性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
7.2 适用场景
✅ 推荐使用:
-
突发流量大的 IO 密集型服务
-
需要兼顾稳定性和弹性的核心业务
-
下游系统有吞吐上限,需要保护的场景
-
有大量 ThreadLocal 上下文需要维护的遗留系统
❌ 不推荐使用:
-
CPU 密集型任务(应该用
Dispatchers.Default) -
极低延迟要求的场景(多了一层判断开销)
-
已经全面拥抱虚拟线程、没有 ThreadLocal 依赖的新项目
-
并发量稳定、没有明显波峰波谷的场景
八、总结
混合调度器的核心价值在于弹性 ------它不是要取代谁,而是在 Dispatchers.IO 的稳定性和虚拟线程的高并发之间找到一个平衡点。
回顾一下它的核心设计:
-
双计数器感知拥挤:不依赖黑盒内部状态,自己精确计算积压
-
IO 优先,VT 溢出:正常流量零开销,高峰时自动弹性扩容
-
ThreadLocal 保鲜:快照式透传,解决虚拟线程的上下文丢失问题
-
五种饱和策略:从入队等待到静默丢弃,覆盖各种业务需求
-
空位转交机制:等待队列的任务优先复用空位,减少调度抖动
-
无锁并发控制 :用
AtomicInteger替代Semaphore,快速路径零阻塞
这个实现虽然只有几百行代码,但涵盖了并发编程中的很多经典问题:竞态条件、丢失唤醒、无锁编程、资源池设计... 值得细细品味。
如果你正在使用 Kotlin 协程处理 IO 密集型任务,不妨试试这种混合调度的思路------也许它能在稳定性和吞吐量之间,给你一个恰到好处的选择。
九、源码
kotlin
// ╔══════════════════════════════════════════════════════════════════════╗
// ║ HybridDispatcher --- 混合调度器 ║
// ║ 架构: IO 优先 → VT 溢出 → 四种饱和策略 ║
// ╚══════════════════════════════════════════════════════════════════════╝
/**
* 当 IO 路径和 VT 路径同时饱和时,后续任务的策略。
*
* - [SUSPEND] ------ 入队等待,不阻塞线程(默认)
* - [DROP] ------ 静默丢弃(记录计数)
* - [REJECT] ------ 抛出 [RejectedExecutionException]
* - [EXPAND] ------ 临时扩大 IO 路径容量(有硬上限,触及后回退 SUSPEND)
* - [EXPAND_VT] ------ 临时扩大 VT 路径容量(有硬上限,触及后回退 SUSPEND)
*/
enum class OverflowStrategy {
SUSPEND, DROP, REJECT, EXPAND, EXPAND_VT,
}
// ───────────────────────────────────────────────────────────────────
// ThreadLocalSnapshot --- VT 路径的 ThreadLocal 保鲜
// ───────────────────────────────────────────────────────────────────
/**
* 携带一组 [ThreadLocal] 的快照,用于跨虚拟线程透传。
*
* 每个 [HybridConfig] 实例维护自己的注册表(不影响全局),
* 只捕获**已注册**的 ThreadLocal。
*
* ## 使用
* ```kotlin
* val snapshot = ThreadLocalSnapshot.template(listOf(MY_TL, MDC_TL)).capture()
* snapshot.apply()
* try { /* VT 上执行 */ } finally { snapshot.clear() }
* ```
* @author : zimo
* @date : 2026/06/24
*/
class ThreadLocalSnapshot private constructor(
private val entries: Array<out Pair<ThreadLocal<*>, Any?>>
) {
/** 从当前线程捕获已注册 ThreadLocal 的值 */
fun capture(): ThreadLocalSnapshot {
val e = entries
val newEntries = Array(e.size) { i ->
val (tl, _) = e[i]
tl to tl.get()
}
return ThreadLocalSnapshot(newEntries)
}
/** 恢复到当前线程 */
@Suppress("UNCHECKED_CAST")
fun apply() {
for ((tl, v) in entries) (tl as ThreadLocal<Any?>).set(v)
}
/** 清除当前线程的对应 ThreadLocal,防止泄露/污染 */
fun clear() {
for ((tl, _) in entries) tl.remove()
}
override fun toString(): String = buildString {
append("ThreadLocalSnapshot{")
entries.forEachIndexed { i, (tl, v) ->
if (i > 0) append(", ")
append("$tl → $v")
}
append("}")
}
companion object {
/** 空快照单例,避免 VT 路径无条件分支 */
val EMPTY = ThreadLocalSnapshot(emptyArray())
/** 从注册表创建快照模板。调用 [capture] 即可获得实际值。 */
fun template(registry: Collection<ThreadLocal<*>>): ThreadLocalSnapshot {
val arr = registry.map { it to null }.toTypedArray()
return ThreadLocalSnapshot(arr)
}
}
}
// ───────────────────────────────────────────────────────────────────
// HybridDispatcher(调度器实现)
// ───────────────────────────────────────────────────────────────────
/**
* 混合调度器实现 ------ IO 线程池优先,感知积压后溢出到虚拟线程。
*
* ## 调度模型
*
* ```
* dispatch(task)
* │
* ├── ioDispatched < ioCeiling && (ioDispatched - ioExecuting) < queueThreshold
* │ → Dispatchers.IO(快速路径:IO 不拥挤,直接投递)
* │
* ├── 积压 ≥ queueThreshold || ioDispatched ≥ ioCeiling
* │ → 尝试 VT 溢出路径(虚拟线程执行,不阻塞载体线程)
* │
* └── VT 也饱和 → overflowStrategy
* ├─ SUSPEND → 入队 pendingQueue 等待(不阻塞线程)
* ├─ DROP → 静默丢弃 + 计数
* ├─ REJECT → throw RejectedExecutionException
* ├─ EXPAND → 临时提升 ioCeiling 再试(有硬上限)
* └─ EXPAND_VT → 临时提升 vtLimit 再试(有硬上限,VT 专属版 EXPAND)
* ```
*
* ## IO 拥挤感知
*
* 不再猜测 Dispatchers.IO 是否有空位,而是用两个计数器精确感知:
*
* - `ioDispatched` --- 已投递给 Dispatchers.IO、尚未完成的任务总数
* - `ioExecuting` --- 正在 IO 线程上实际执行的数量
* - **差值** ≈ Dispatchers.IO 内部积压队列的长度
*
* 差值积累到 `queueThreshold` 时,说明 IO 路径确实拥挤,开始溢出到虚拟线程。
*
* ## ioCeiling 的默认值
*
* 默认等于 `Dispatchers.IO` 的真实并行度(读 `kotlinx.coroutines.io.parallelism` 系统属性,
* 即 `max(64, CPU 核数)`),与协程框架保持一致。设 0 取消入场上限。
*
* ## 对外入口
*
* 请使用 [Dispatchers.Hybrid](全局单例)或 [HybridConfig](自建实例)。
*
* @see HybridConfig
* @author : zimo
* @date : 2026/06/24
*/
class HybridDispatcher internal constructor(
internal val config: HybridConfig,
) : CoroutineDispatcher(), Closeable {
// 常量
companion object {
/** 默认排队阈值 */
const val DEFAULT_QUEUE_THRESHOLD = 16
/** 默认不限制虚拟线程 */
const val DEFAULT_MAX_VIRTUAL_THREADS = Int.MAX_VALUE
// System property keys
const val PROP_IO_CEILING = "kotlinx.coroutines.hybrid.io.ceiling"
const val PROP_QUEUE_THRESHOLD = "kotlinx.coroutines.hybrid.queue.threshold"
const val PROP_MAX_VIRTUAL_THREADS = "kotlinx.coroutines.hybrid.virtual.max"
const val PROP_OVERFLOW_STRATEGY = "kotlinx.coroutines.hybrid.overflow.strategy"
const val PROP_EXPAND_MAX = "kotlinx.coroutines.hybrid.expand.max"
/**
* IO 并行度------与 Dispatchers.IO 内部的线程数保持一致。
*
* 读取逻辑与 kotlinx.coroutines 内部完全一致:
* 1. 若设置了 `kotlinx.coroutines.io.parallelism` → 使用之
* 2. 否则 = max(64, CPU 核数)
*/
val IO_PARALLELISM: Int by lazy {
System.getProperty("kotlinx.coroutines.io.parallelism")?.toIntOrNull()
?: maxOf(64, Runtime.getRuntime().availableProcessors())
}
}
// ── 并发控制 ──
//
// 两个计数器区分 "发出去了" 和 "真的在跑了":
// ioDispatched = 已交给 Dispatchers.IO 但尚未完成的数量
// ioExecuting = 正在 IO 线程上实际执行的数量
//
// 差值 = Dispatchers.IO 内部积压队列的长度。
// 当差值 >= queueThreshold 时,说明 IO 路径拥挤,触发 VT 溢出。
/** 已派发到 IO 路径、尚未完成的总数 */
private val ioDispatched = AtomicInteger(0)
/** 正在 IO 线程上执行的数量(在 run() 开始时 +1,结束时 -1) */
private val ioExecuting = AtomicInteger(0)
/** IO 路径准入硬上限(可配),防止 ioDispatched 无限增长 */
@Volatile
var ioCeiling: Int = config.ioCeiling
/** 排队容忍度:ioDispatched - ioExecuting >= 此值时溢出到 VT */
@Volatile
var queueThreshold: Int = config.queueThreshold
/** VT 路径 in-flight 计数 */
private val vtInFlight = AtomicInteger(0)
/** VT 路径当前上限(`null`=不限制)。EXPAND_VT 策略下会上浮。 */
@Volatile
var vtLimit: Int? = if (config.maxVirtualThreads < Int.MAX_VALUE) config.maxVirtualThreads else null
/** 等待队列(全路径饱和时暂存) */
private data class PendingTask(val context: kotlin.coroutines.CoroutineContext, val runnable: Runnable)
private val pendingQueue = ConcurrentLinkedQueue<PendingTask>()
// ── 调度器组件 ──
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
@Volatile private var vtExecutor: ExecutorService? = null
@Volatile private var vtDispatcher: CoroutineDispatcher? = null
// ── ThreadLocal 保鲜 ──
internal val threadLocalRegistry = ConcurrentLinkedQueue<ThreadLocal<*>>()
@Volatile internal var tlTemplate = ThreadLocalSnapshot.EMPTY
// ── 统计 ──
private val ioTaskCount = AtomicLong(0)
private val vtTaskCount = AtomicLong(0)
private val droppedCount = AtomicLong(0)
private val pendingCount = AtomicLong(0)
val ioCount: Long get() = ioTaskCount.get()
val vtCount: Long get() = vtTaskCount.get()
val dropped: Long get() = droppedCount.get()
val pending: Long get() = pendingCount.get()
// ═════════════════════════════════════════════════════════════
// dispatch() --- 热路径
// ═════════════════════════════════════════════════════════════
//
// 三级调度 + VT 扩容:
// 1. IO 不拥挤 → 直接走 Dispatchers.IO(快速路径)
// 2. IO 积压 ≥ queueThreshold → 溢出到虚拟线程
// 3. VT 也饱和 → EXPAND_VT 尝试临时扩容 VT,否则按 overflowStrategy 处理
override fun dispatch(context: kotlin.coroutines.CoroutineContext, block: Runnable) {
// ── 准入检查:ioDispatched 是否触及硬上限 ──
val ceiling = ioCeiling
if (ceiling > 0 && ioDispatched.get() >= ceiling) {
// IO 路径已达配置上限 → 直接走溢出
overflowOrEnqueue(context, block)
return
}
// ── 计算 IO 路径拥挤度 ──
val dispatched = ioDispatched.incrementAndGet()
val executing = ioExecuting.get()
val queued = dispatched - executing // ≈ Dispatchers.IO 内部积压数
if (queued <= queueThreshold) {
// IO 路径健康:队列不长,走 IO
ioTaskCount.incrementAndGet()
ioDispatcher.dispatch(context, IOWrapper(block))
} else {
// IO 路径拥挤:队列过长 → 回退计数,溢出
ioDispatched.decrementAndGet()
overflowOrEnqueue(context, block)
}
}
/** IO 路径不可用时的统一入口:先试 VT,不行走策略 */
private fun overflowOrEnqueue(context: kotlin.coroutines.CoroutineContext, block: Runnable) {
// 尝试 VT 溢出
if (VT_AVAILABLE) {
val acquired = tryAcquireVT()
if (acquired) {
dispatchToVT(context, block)
return
}
// VT 满了 → EXPAND_VT 尝试扩容
if (config.overflowStrategy == OverflowStrategy.EXPAND_VT) {
val newLimit = (vtLimit ?: 0) + 1
if (newLimit <= config.vtExpandHardLimit) {
vtLimit = newLimit
// 扩容后重试 VT
if (tryAcquireVT()) {
dispatchToVT(context, block)
return
}
}
}
}
// 全路径饱和 → 按策略
when (config.overflowStrategy) {
OverflowStrategy.SUSPEND, OverflowStrategy.EXPAND_VT -> {
// EXPAND_VT 也满了 → 回退 SUSPEND
pendingQueue.offer(PendingTask(context, block))
pendingCount.incrementAndGet()
}
OverflowStrategy.DROP -> droppedCount.incrementAndGet()
OverflowStrategy.REJECT -> throw RejectedExecutionException(
"HybridDispatcher saturated: ioDispatched=$ioDispatched ioExecuting=$ioExecuting vtInFlight=$vtInFlight"
)
OverflowStrategy.EXPAND -> {
ioCeiling += 1
if (ioCeiling <= config.expandHardLimit) {
// 扩容后重试 IO 路径
if (ioDispatched.incrementAndGet() <= ioCeiling) {
ioTaskCount.incrementAndGet()
ioDispatcher.dispatch(context, IOWrapper(block))
return
}
ioDispatched.decrementAndGet()
}
pendingQueue.offer(PendingTask(context, block))
pendingCount.incrementAndGet()
}
}
}
/** 尝试获取一个 VT 配额。成功时 vtInFlight 已 +1,调用方负责最终 -1。 */
private fun tryAcquireVT(): Boolean {
val limit = vtLimit
if (limit == null) return true // unlimited
return vtInFlight.incrementAndGet() <= limit
}
/** 将任务投递到 VT 路径(已持有 vtInFlight 配额) */
private fun dispatchToVT(context: kotlin.coroutines.CoroutineContext, block: Runnable) {
vtTaskCount.incrementAndGet()
val tlSnapshot = if (threadLocalRegistry.isNotEmpty()) {
runCatching { tlTemplate.capture() }.getOrDefault(ThreadLocalSnapshot.EMPTY)
} else {
ThreadLocalSnapshot.EMPTY
}
ensureVTDispatcher().dispatch(context, VTWrapper(block, tlSnapshot))
}
// ═════════════════════════════════════════════════════════════
// 任务完成回调
// ═════════════════════════════════════════════════════════════
/**
* IO 任务完成后调用:优先将空位转交给等待队列中的任务,
* 否则归还 ioDispatched 计数。
*/
private fun onIOTaskComplete() {
val pending = pendingQueue.poll()
if (pending != null) {
pendingCount.decrementAndGet()
ioTaskCount.incrementAndGet()
// 空位转交,ioDispatched 计数不增减
ioDispatcher.dispatch(pending.context, IOWrapper(pending.runnable))
} else {
ioDispatched.decrementAndGet()
// 二次检查:防丢失唤醒
val retry = pendingQueue.poll()
if (retry != null) {
if (ioDispatched.incrementAndGet() <= ioCeiling) {
pendingCount.decrementAndGet()
ioTaskCount.incrementAndGet()
ioDispatcher.dispatch(retry.context, IOWrapper(retry.runnable))
return
}
// 已达上限,放回队列
pendingQueue.offer(retry)
ioDispatched.decrementAndGet()
}
}
}
// ═════════════════════════════════════════════════════════════
// Runnable 包装
// ═════════════════════════════════════════════════════════════
private inner class IOWrapper(private val delegate: Runnable) : Runnable {
override fun run() {
ioExecuting.incrementAndGet() // ← 真正开始执行
try {
delegate.run()
} finally {
ioExecuting.decrementAndGet()
onIOTaskComplete() // ← 完成(内含 ioDispatched--)
}
}
}
private inner class VTWrapper(
private val delegate: Runnable,
private val tlSnapshot: ThreadLocalSnapshot,
) : Runnable {
override fun run() {
if (tlSnapshot !== ThreadLocalSnapshot.EMPTY) tlSnapshot.apply()
try {
delegate.run()
} finally {
if (tlSnapshot !== ThreadLocalSnapshot.EMPTY) tlSnapshot.clear()
if (vtLimit != null) vtInFlight.decrementAndGet()
}
}
}
// ═════════════════════════════════════════════════════════════
// 生命周期
// ═════════════════════════════════════════════════════════════
override fun close() {
synchronized(this) {
vtDispatcher = null
vtExecutor?.close()
vtExecutor = null
}
pendingQueue.clear()
}
private fun ensureVTDispatcher(): CoroutineDispatcher {
vtDispatcher?.let { return it }
synchronized(this) {
vtDispatcher?.let { return it }
val executor = Executors.newVirtualThreadPerTaskExecutor()
vtExecutor = executor
val dispatcher = executor.asCoroutineDispatcher()
vtDispatcher = dispatcher
return dispatcher
}
}
// ── 诊断 ──
fun snapshot(): String = buildString {
val d = ioDispatched.get()
val e = ioExecuting.get()
append("Hybrid[")
append("ioRunning=$e, ioQueued=${d - e}, ioCeiling=$ioCeiling, qThreshold=$queueThreshold")
if (VT_AVAILABLE) {
val vtCur = vtInFlight.get()
val vtMax = vtLimit
if (vtMax != null) {
val orig = config.maxVirtualThreads
append(", vtInFlight=$vtCur/$vtMax")
if (vtMax > orig) append("(expanded from $orig)")
} else {
append(", vtInFlight=$vtCur/unlimited")
}
}
append(", pending~=$pending, dropped=$dropped")
append(", strategy=${config.overflowStrategy}")
append("]")
}
override fun toString(): String = buildString {
val d = ioDispatched.get()
val e = ioExecuting.get()
append("HybridDispatcher(")
append("ioCeiling=$ioCeiling, ")
append("queueThreshold=$queueThreshold, ")
append("maxVirtualThreads=")
if (config.maxVirtualThreads == Int.MAX_VALUE) append("unlimited") else append(config.maxVirtualThreads)
append(", overflowStrategy=${config.overflowStrategy}")
append(", vtAvailable=$VT_AVAILABLE")
append(", ioRunning=$e, ioQueued=${d - e}")
append(", ioCount=$ioCount, vtCount=$vtCount")
append(", dropped=$dropped, pending~=$pending")
append(")")
}
}
// ───────────────────────────────────────────────────────────────────
// HybridConfig --- 可配置的 Dispatchers.Hybrid 单例
// ───────────────────────────────────────────────────────────────────
/**
* [Dispatchers.Hybrid] 返回此类型,既是调度器又是配置入口。
*
* ## 使用
*
* ```kotlin
* // ------ 配置(应用启动时调一次即可) ------
* Dispatchers.Hybrid.ioCeiling = 128
* Dispatchers.Hybrid.queueThreshold = 32
* Dispatchers.Hybrid.maxVirtualThreads = 5_000
* Dispatchers.Hybrid.overflowStrategy = OverflowStrategy.DROP
* Dispatchers.Hybrid.registerThreadLocal(MyContext.THREAD_LOCAL)
*
* // ------ 使用 ------
* withContext(Dispatchers.Hybrid) {
* jdbcTemplate.query(...) // 阻塞 JDBC 调用
* }
* ```
*
* **注意**:所有配置属性均为 `var`,修改后通过 setter 实时同步到内部 delegate,
* 对后续调度的新任务即时生效。建议在应用启动早期完成配置以避免短暂的不一致窗口。
* @author : zimo
* @date : 2026/06/24
*/
class HybridConfig internal constructor() : CoroutineDispatcher(), Closeable {
// ── 可配置参数 ──
/**
* IO 路径准入硬上限。
*
* 默认值 = [HybridDispatcher.IO_PARALLELISM],与 Dispatchers.IO 的线程数一致
* (通常 = max(64, CPU 核数))。
* 设为 0 表示不限制。
*/
@Volatile
var ioCeiling: Int = HybridDispatcher.IO_PARALLELISM
set(value) {
field = value
_delegate?.ioCeiling = value
}
/**
* IO 路径排队容忍度。
*
* 当 `ioDispatched - ioExecuting >= queueThreshold` 时触发 VT 溢出。
* 默认 16。
*/
@Volatile
var queueThreshold: Int = HybridDispatcher.DEFAULT_QUEUE_THRESHOLD
set(value) {
field = value
_delegate?.queueThreshold = value
}
/** 虚拟线程上限。[Int.MAX_VALUE] 表示不限制。 */
@Volatile
var maxVirtualThreads: Int = HybridDispatcher.DEFAULT_MAX_VIRTUAL_THREADS
/** 全路径饱和时的策略。默认 [OverflowStrategy.SUSPEND]。 */
@Volatile
var overflowStrategy: OverflowStrategy = OverflowStrategy.SUSPEND
/** EXPAND 策略 IO 硬上限。默认 ioCeiling * 4。 */
var expandHardLimit: Int
get() = _expandHardLimit ?: (ioCeiling * 4).coerceAtLeast((ioCeiling + queueThreshold) * 2)
set(value) { _expandHardLimit = value }
private var _expandHardLimit: Int? = null
/** EXPAND_VT 策略 VT 硬上限。默认 maxVirtualThreads * 2(最低 100)。 */
var vtExpandHardLimit: Int
get() = _vtExpandHardLimit ?: (maxVirtualThreads * 2).coerceAtLeast(100)
set(value) { _vtExpandHardLimit = value }
private var _vtExpandHardLimit: Int? = null
// ── 内部委托 ──
@Volatile
private var _delegate: HybridDispatcher? = null
/**
* 内部 delegate 的惰性引用。
*
* 首次 dispatch 时构造,此后配置修改通过 setter 直接同步到 delegate。
*/
private val delegate: HybridDispatcher
get() {
_delegate?.let { return it }
synchronized(this) {
_delegate?.let { return it }
val d = HybridDispatcher(this)
_delegate = d
return d
}
}
// ── 调度转发 ──
override fun dispatch(context: kotlin.coroutines.CoroutineContext, block: Runnable) {
delegate.dispatch(context, block)
}
// ── ThreadLocal 保鲜 ──
/** 注册需要在 VT 路径透传的 [ThreadLocal]。线程安全,可动态注册。 */
fun registerThreadLocal(vararg tls: ThreadLocal<*>) {
val d = delegate
tls.forEach { d.threadLocalRegistry.add(it) }
d.tlTemplate = ThreadLocalSnapshot.template(d.threadLocalRegistry)
}
// ── 统计 ──
val ioCount: Long get() = _delegate?.ioCount ?: 0
val vtCount: Long get() = _delegate?.vtCount ?: 0
val dropped: Long get() = _delegate?.dropped ?: 0
val pending: Long get() = _delegate?.pending ?: 0
// ── 生命周期 ──
override fun close() {
synchronized(this) {
_delegate?.close()
_delegate = null
}
}
fun snapshot(): String = _delegate?.snapshot() ?: "HybridConfig(not initialized)"
override fun toString(): String = buildString {
append("HybridConfig(")
append("ioCeiling=$ioCeiling, ")
append("queueThreshold=$queueThreshold, ")
append("maxVirtualThreads=")
if (maxVirtualThreads == Int.MAX_VALUE) append("unlimited") else append(maxVirtualThreads)
append(", overflowStrategy=$overflowStrategy")
append(", vtAvailable=$VT_AVAILABLE")
append(", ioCount=$ioCount, vtCount=$vtCount")
append(", dropped=$dropped, pending~=$pending")
append(")")
}
}
// ╔══════════════════════════════════════════════════════════════════════╗
// ║ Dispatchers 扩展 ║
// ╚══════════════════════════════════════════════════════════════════════╝
/** 混合调度器默认单例,JVM 退出时自动关闭。 */
private val hybridConfigInstance by lazy {
HybridConfig().also { config ->
Runtime.getRuntime().addShutdownHook(
Thread({ config.close() }, "HybridConfig-shutdown")
)
}
}
/**
* 混合调度器(默认单例)------ IO 优先,溢出到虚拟线程。
*
* ## 即用
* ```kotlin
* withContext(Dispatchers.Hybrid) { jdbcTemplate.query(...) }
* ```
*
* ## 配置示例
* ```kotlin
* Dispatchers.Hybrid.ioCeiling = 128 // IO 路径并发上限
* Dispatchers.Hybrid.queueThreshold = 32 // 排队深度
* Dispatchers.Hybrid.maxVirtualThreads = 5_000 // VT 上限
* Dispatchers.Hybrid.overflowStrategy = OverflowStrategy.DROP // 饱和策略
* Dispatchers.Hybrid.registerThreadLocal(MY_TL) // VT 路径 ThreadLocal 透传
* ```
*
* JDK < 21 时自动回退到纯 IO 路径(SUSPEND 策略)。
* @author : zimo
* @date : 2026/06/24
*/
val Dispatchers.Hybrid: HybridConfig get() = hybridConfigInstance
/**
* 创建自定义配置的混合调度器实例。
*
* **自建实例须在不用时调用 [HybridConfig.close] 或以 `use {}` 包裹。**
*
* ```kotlin
* val custom = Dispatchers.hybrid(
* ioCeiling = 128,
* queueThreshold = 32,
* maxVirtualThreads = 10_000,
* overflowStrategy = OverflowStrategy.EXPAND,
* )
* ```
*
* @param ioCeiling IO 路径最大并发,默认 64
* @param queueThreshold IO 路径排队容量,超出后溢出,默认 16
* @param maxVirtualThreads 虚拟线程上限,默认无限制
* @param overflowStrategy 全路径饱和策略,默认 SUSPEND
* @author : zimo
* @date : 2026/06/24
*/
fun Dispatchers.hybrid(
ioCeiling: Int = HybridDispatcher.IO_PARALLELISM,
queueThreshold: Int = HybridDispatcher.DEFAULT_QUEUE_THRESHOLD,
maxVirtualThreads: Int = HybridDispatcher.DEFAULT_MAX_VIRTUAL_THREADS,
overflowStrategy: OverflowStrategy = OverflowStrategy.SUSPEND,
): HybridConfig = HybridConfig().apply {
this.ioCeiling = ioCeiling
this.queueThreshold = queueThreshold
this.maxVirtualThreads = maxVirtualThreads
this.overflowStrategy = overflowStrategy
}