定时器升级为分级时间轮

前言

之前skynet_fly的定时器是用一个间隔60秒的check循环来实现的,这样做的好处是避免了skynet注册长时间定时器占用lua携程的问题。但随着使用场景的增多,这种实现暴露出了一些不足:1是精度被限制在check间隔内;2是取消操作需要遍历查找;3是大量定时器时check开销增大。所以决定把底层换成经典的分级时间轮(Hierarchical Timing Wheel),对标Linux内核的实现方式,在保证O(1)创建和取消的前提下,大幅提升性能。

旧实现的问题

旧的实现方式:

1
2
3
4
5
6
7
-- 伪代码
while not is_cancel do
skynet.sleep(6000) -- 60秒check一次
if now >= target_time then
callback()
end
end

优点

  • 避免了skynet.timeout长时间占用携程

缺点

  • 取消定时器需要标记后等待下次check才真正停止
  • 精度最高60秒(虽然实际可以更短的check间隔,但太短又增加开销)
  • 大量定时器时遍历check开销 O(n)
  • 延长定时器需要额外计算

分级时间轮实现

数据结构

借鉴Linux内核经典的4级时间轮结构:

层级 槽数 覆盖范围 精度
tv1 256 [cur, cur+255] 1 tick
tv2 64 [cur+256, cur+16383] 256 ticks
tv3 64 [cur+16384, cur+1048575] 16384 ticks
tv4 64 [cur+1048576, cur+67108863] 1048576 ticks

最大可调度延迟:67108863 ticks ≈ 671088 秒 ≈ 7.77天

核心操作复杂度

操作 复杂度 说明
create O(1) 根据延迟时间直接定位槽位
cancel O(1) 双向链表直接摘除,无搜索
extend O(1) cancel + create
dispatch O(k) k = 当前tick到期数量

关键实现思路

1. 定位槽位

根据到期时间和当前游标的差值,决定插入哪一级的哪个槽:

  • 差值 < 256 → tv1
  • 差值 < 16384 → tv2
  • 差值 < 1048576 → tv3
  • 否则 → tv4

2. 级联(Cascade)

当时间推进时,低级轮转完一圈,需要从高级轮取出对应槽的定时器重新分配到低级轮。这就是”cascade”操作,类似钟表的进位。

3. 驱动调度

使用skynet.timeout作为底层tick驱动。通过维护一个pending_expirepending_version

  • 只注册一个skynet.timeout指向最近到期时间
  • 当新定时器比当前pending更早到期时,用version机制让旧timeout自动失效
  • 空闲时用心跳tick保持轮转(最长60秒间隔)

4. 双向链表

每个槽是一个双向链表,定时器节点直接挂载。取消时直接从链表摘除,O(1)完成,无需搜索。

性能优化

P0: 消除dispatch的table分配

cascade过程中需要暂存定时器列表。使用栈式复用的pending_readd数组,避免每次dispatch都创建新table。

P0: 回调同步执行

回调使用xpcall直接执行,不fork新携程,减少携程创建开销。

P0: skynet.timeout局部缓存

1
local skynet_timeout = skynet.timeout

减少全局查找(注意skynet.now不缓存,兼容录像系统)。

P2: 对象池复用

已完成/已取消的定时器table放入对象池,下次new时直接取出复用,减少GC压力。

1
2
-- 默认最大缓存256个
M.set_pool_max(256)

引用计数策略:用户通过release()主动归还,忘记调用则由GC自然回收。

新增API

相比旧版本,新增了以下便捷API:

API 说明
M:once(expire, callback, ...) 创建单次定时器
M:new_loop(expire, callback, ...) 创建循环定时器
timer_obj:is_cancelled() 是否已取消
timer_obj:is_finished() 是否已完成
timer_obj:is_loop() 是否循环
timer_obj:remain_times() 剩余次数
timer_obj:is_valid() 是否仍有效
timer_obj:release() 释放对象供池复用
M.set_warn_threshold(ticks) 设置耗时告警阈值
M.set_pool_max(max) 设置对象池大小

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
local timer = require "skynet-fly.timer"

-- 便捷API
local t = timer:once(timer.second * 5, function()
log.info("5秒后触发")
end)

-- 循环定时器
local loop = timer:new_loop(timer.second * 2, function()
log.info("每2秒")
end)

-- 查询状态
print(t:remain_expire()) -- 剩余tick
print(loop:is_valid()) -- true

-- 延长
t:extend(timer.second * 3)

-- 取消
loop:cancel()

-- 释放回池
t:release()

时间常量

1
2
3
4
5
M.loop   = 0          -- 循环标记
M.second = 100 -- 1秒
M.minute = 6000 -- 1分钟
M.hour = 360000 -- 1小时
M.day = 8640000 -- 1天

API文档

skynetfly源码地址


定时器升级为分级时间轮
https://huahua132.github.io/2026/05/15/skynet_fly_ss/timer_wheel/
作者
huahua132
发布于
2026年5月15日
许可协议