当前位置: 首页 > news >正文

固定数组时间轮的槽过载优化:桶链表与批次执行

固定数组时间轮的槽过载优化:桶链表与批次执行

时间轮是处理海量定时任务的标准结构,它把时间切分成固定长度的槽,每个槽挂一个定时器链表。tick 推进时扫描当前槽链表,触发所有到期任务。理想情况下,每次 tick 的复杂度是 O(K),K 为该槽上的到期定时器数,通常是常数。但当瞬时大量任务堆积在同一个槽时——比如 1ms 精度的轮子,某个时刻同时到了上万个超时——遍历链表会拖慢整个 tick,后续槽的处理被推迟,定时精度整体恶化。

这篇笔记记录一种简单直接的工程优化:让一个槽可以挂多个子桶(桶链表),并限制每次 tick 触发的任务数量,用可接受的延迟抖动换取处理时间的上界。


1. 基础结构:数组 + 双向链表

先看最简模型。一个时间轮就是一个定长数组slots[N],每个元素是双向链表的头指针。定时器节点内嵌prev/next指针,插入时计算到期槽位(cur + delay_ticks) % N并加入链表头,tick 时遍历该槽链表、触发回调并摘除。数组大小固定,没有动态分配。

当槽内定时器很少时,这完全够用。


2. 槽过载:一个桶装不下

当某个槽的链表过长,单次 tick 遍历成本可能超过一个槽的时长(比如 1ms),形成雪崩。直观解法是横向扩展——一个槽不再只是一个链表,而是一串“小桶”,每个小桶内部又是一个定时器链表。

原结构: slot[i] -> Node -> Node -> ... (单链表) 新结构: slot[i] -> Bucket1 -> Bucket2 -> ... | | v v Node链表 Node链表

桶单元(Bucket)是一个固定或动态分配的小结构:

  • 内部包含一个定时器双向链表的头指针(和尾指针,便于尾部插入)。
  • 一个next指针,指向同槽的下一个桶单元。
  • 可选:当前桶内的定时器数量,用于快速判断是否已满。

这样就把一个长链表切分成了多个短链表,每个桶单元内部长度可控(比如限制最多 128 个节点)。当某个桶满了,就从预分配的对象池里取一个空桶,挂到桶链尾部。


3. 插入定时器:选定桶并挂载

插入流程不变:先根据延迟计算出目标槽索引idx。然后找到该槽的桶链,一般是追加到最后一个桶单元(这样可以保证先入先出)。如果最后一个桶已满,就新建一个桶单元接在后面,再插入。

伪逻辑:

bucket = slots[idx].tail_bucket; if bucket is full: bucket = alloc_bucket(); slots[idx].tail_bucket.next = bucket; slots[idx].tail_bucket = bucket; insert timer node into bucket.timer_list;

这里完全保持了 O(1) 插入。


4. 批次执行:每次 tick 只触发固定数量

为了避免一次 tick 触发整个槽的几百上千个任务,我们引入一个批次上限BATCH_SIZE(例如 128)。每次 tick 处理当前槽时,只触发最多BATCH_SIZE个任务,然后停止。未处理完的任务继续留在桶内,等待下次 tick 再处理。

这会使部分任务的真实触发时间延后几个 tick,但只要批次上限设置合理(比如最大延迟 = BATCH_SIZE × tick_interval),这个额外延迟是可量化的、受控的。对于大多数超时场景(如 30s 连接超时,1ms tick),延时几个毫秒完全可以接受。

处理流程(每个 tick):

  1. 取当前槽索引cur = (cur + 1) % N
  2. 设置计数器processed = 0
  3. 从该槽的第一个桶单元开始,遍历其内部定时器链表。
  4. 对于每个定时器,如果已到期且未取消,触发回调,从链表删除,processed++;如果processed == BATCH_SIZE,记录当前桶和节点位置(断点),结束本轮。
  5. 如果当前桶处理完毕,移到下一个桶继续,直到批次用完或所有桶清空。
  6. 下次 tick 到同一个槽时,从断点继续处理。

这样,每次 tick 的工作量严格受BATCH_SIZE控制,保证了时间轮的推进节奏不会被突然的流量高峰打乱。


5. 线程安全要点
  • 生产者-消费者模型:业务线程插入定时器(生产者),专用 tick 线程负责推进和触发(消费者)。
  • 锁粒度:可以为每个槽设置一把锁,或者为每个桶单元设置锁。桶锁粒度更细。
  • 惰性删除:取消定时器时仅打标记,由 tick 线程遍历时真正移除,避免并发删除的复杂性。
  • 对象池:桶单元和定时器节点都从预分配池中获取,消除动态内存分配导致的延迟抖动。

6. Mermaid 结构图

数据结构示意:

时间轮数组

slot 0

Bucket 1

slot 1

Bucket 1

Bucket 2

slot 2

...

TimerNode

TimerNode

TimerNode

TimerNode

TimerNode

TimerNode

TimerNode

批次 tick 流程:

开始 tick

推进 current_slot 指针

取该槽第一个桶单元

本轮已触发数 < BATCH_SIZE?

桶内还有未处理节点?

触发一个到期定时器

processed++

有下一个桶?

移动到下一个桶

该槽处理完毕

保存断点,停止

结束 tick


7. 总结

这套方案本质上是用空间(桶链表)换时间确定性,用可接受的延迟抖动(批次导致的滞后)换处理时间上界。它保留了固定数组时间轮 O(1) 插入和 O(1) 删除的优点,同时解决了单槽过载带来的长尾延迟问题,非常适合海量定时器、高并发、对延迟抖动容忍度较高的场景(如网络框架超时管理、游戏定时事件)。

http://www.rkmt.cn/news/1525231.html

相关文章:

  • GR3-Fourier V10.3~V10.9版本的底层驱动算法源码和工业硬件参数标定数据。算法部分涵盖Park变换、斜坡限幅、定时器配置等10个核心功能模块(1-25号)。硬件参数部分详细列出了26
  • 别再傻傻用ManualResetEvent了!C#高并发场景下,试试这个性能更强的轻量级替代品
  • 终极MTK设备底层调试与刷机完全指南
  • 2026年除尘器滤芯厂家靠谱推荐@拿货质保认准滤芯芳姐? - 速递信息
  • 整数溢出陷阱:用除法安全比较乘积
  • NSK LPFC 1616-3 高刚性零背隙滚珠丝杠技术解析
  • Google 爬虫工作原理,及用Python实现完整的Google爬虫
  • Wayback Machine浏览器扩展:让消失的网页永远触手可及的数字时光机
  • 你的会议麦克风真的‘智能’吗?拆解ANS噪声抑制在腾讯会议、Zoom里的实际表现
  • 基于MATLAB的静止无功补偿系统设计3(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_文章底部可以扫码
  • 终极LRC歌词批量下载指南:10分钟让离线音乐库焕发新生
  • 西湖区处理全套附件大牌包,奢二网专业鉴定爱马仕Kelly、铂金包 - 讯息早知道
  • 如何在3小时内搭建你自己的怀旧传奇服务器:OpenMir2终极指南
  • 从零开始打造你的AI角色:SillyTavern角色卡片完全指南
  • 2026青岛首饰回收行业实测:行情解析与避坑白皮书 - 逸程
  • AI 辅助的前端国际化文案本地化策略:从机械翻译到语境适配,多语言产品的智能交付
  • MPC8309 eLBC FCM硬件控制器驱动NAND Flash原理与实践
  • 广州天河区搬家工人闲置摆摊:盒饭从6元降到免费,同城搬家行业供需困局深度解析 - 从来都是英雄出少年
  • 线上三大和田玉品牌对比测评:优选肖氏珠宝 - 速递信息
  • 杭州临安区闲置名牌包包线下估价|奢二网全覆盖一线箱包,本地网点估价透明不压价 - 讯息早知道
  • Vue 3 Teleport 与异步组件深度实践:从 DOM 约束到逻辑自由,组件架构的灵活性跃迁
  • 2026济南宝格丽首饰回收指南:新手全流程实操手册 - 薛定谔的梨花猫
  • 2026降AI率平台实测:10款网站对比,论文质量提升秘籍 - 降AI小能手
  • 【信息科学与工程学】【通信工程】第二百零一篇 路由器设备中的学科知识01
  • OpenHands 新手实战:开源版 Devin 如何读取项目、修改代码、运行测试?
  • MPC8245 JTAG与监视点:硬件级调试的实战指南
  • 5分钟掌握网盘直链下载助手:8大平台高速下载的终极指南
  • 闲置翡翠回血避坑!青岛 6 家同城回收门店亲测甄选 - 讯息早知道
  • String的isEmpty与equals(““)的区别
  • 专业定制超级电容器公司推荐 - 品牌排行榜