深入理解 Kotlin 协程:从零实现一个 IO 优先 + 虚拟线程溢出的混合调度器

深入理解 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 设计理念

混合调度器的设计遵循三个原则:

  1. 快速路径优先 :正常流量下走 Dispatchers.IO,零额外开销

  2. 弹性溢出:IO 路径拥挤时自动溢出到虚拟线程,不阻塞调用方

  3. 优雅降级:双路径都饱和时,按配置策略处理,而非简单抛异常

1.2 核心组件

组件 职责
HybridDispatcher 调度器核心实现,维护所有状态与计数器
HybridConfig 配置入口,支持运行时动态调参
ThreadLocalSnapshot VT 路径的 ThreadLocal 透传工具
OverflowStrategy 饱和策略枚举

1.3 为什么不直接全用虚拟线程?

你可能会问:既然虚拟线程这么强,为什么不直接全部走虚拟线程?

原因有三:

  1. ThreadLocal 兼容性:大量现有库(如 MDC、Spring 事务上下文)依赖 ThreadLocal,虚拟线程切换会丢失上下文

  2. 池化资源复用:数据库连接池、HTTP 连接池等资源与线程绑定,虚拟线程的海量创建可能击穿池化资源

  3. 成熟度考量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 队列。在竞争激烈或快速路径上,tryAcquirerelease 会走入 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()  // 回退

这里的核心差别在于:AtomicIntegerincrementAndGet 在 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 的设计是两段式的:

  1. 模板(template):包含已注册的 ThreadLocal 列表,但值为 null

  2. 捕获(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 的稳定性和虚拟线程的高并发之间找到一个平衡点。

回顾一下它的核心设计:

  1. 双计数器感知拥挤:不依赖黑盒内部状态,自己精确计算积压

  2. IO 优先,VT 溢出:正常流量零开销,高峰时自动弹性扩容

  3. ThreadLocal 保鲜:快照式透传,解决虚拟线程的上下文丢失问题

  4. 五种饱和策略:从入队等待到静默丢弃,覆盖各种业务需求

  5. 空位转交机制:等待队列的任务优先复用空位,减少调度抖动

  6. 无锁并发控制 :用 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
}
相关推荐
雨落倾城夏未凉1 小时前
第四章c#方法-参数数组和可选参数(16)
后端·c#
陈随易3 小时前
VSCode古法神器fnMap v9开发故事
前端·后端·程序员
用户298698530144 小时前
Java 实现 Word 文档文本查找与高亮标注
java·后端
雪隐4 小时前
个人电脑玩AI-06让5060 Ti给你打工——Qwen3.6-35B-A3B + LM Studio + openWebUI
人工智能·后端
卷无止境4 小时前
现代 C++特性大盘点:一门脱胎换骨的老语言
c++·后端
Ausra无忧4 小时前
记录在公司把单服务器升级成多服务器架构流程
前端·后端·架构
XiaoYuanCode4 小时前
Spring Cloud Alibaba实战01|Nacos入门服务注册与配置中心
后端
宇宙之一粟4 小时前
乐企版式文件生成平台
java·后端·python