作为一名程序员,虽然在写业务逻辑代码时,定时器代码实现好了,然而,乱用定时器也会导致性能降低,所以为了项目的性能考虑,需要我们去了解一下定时器的实现和使用注意。
要了解Skynet定时器,那就需要了解从调用skynet.timeout,到定时器的回调函数被调用的整个过程是怎样的。
Skynet的定时器本质是一个分层时间轮,由独立的timer线程驱动,默认精度是10ms一个tick。 timer线程是使用C语言实现的。
核心流程是:
1.Lua层向C层注册定时器,拿到一个session;
2.Lua层拿到session后,创建一个协程,把session->coroutine存起来;
3.C层把定时器节点按到期时间挂入时间轮的对应槽位;
4.timer线程每10ms推进一次时间轮;
5.定时器到期后,timer线程把一条超时信息投递到目标服务的私有消息队列;
6.worker线程处理该服务时,根据session找到协程并resume;
7.协程执行skynet.timeout传入的回调函数;
整个流程就是这样,接下来我们去看看源码,看看源码的具体代码实现。
1.Lua层向C层注册定时器,拿到一个session;
2.Lua层拿到session后,创建一个协程,把session->coroutine存起来;
Lua调用到C层执行
\skynet\lualib\skynet.lua
function skynet.timeout(ti, func)
local session = auxtimeout(ti)
assert(session)
local co = co_create_for_timeout(func, ti)
assert(session_id_coroutine[session] == nil)
session_id_coroutine[session] = co
return co -- for debug
end
含义:
· function skynet.timeout(ti, func)
· 对外暴露的定时器 API。
· ti 是延迟时间,单位通常是 1/100 秒,默认一个 tick 是 10ms,所以 ti=1 约等于 10ms。
· func 是到期后要执行的回调函数。
· local session = auxtimeout(ti)
· auxtimeout 是 Lua 层对 C 命令的包装,本质上会调用:
cintcommand("TIMEOUT", ti)
· 它会进入 C 层 skynet_command -> cmd_timeout -> skynet_timeout。
· 返回一个 session,用于后续匹配超时响应。
· assert(session)
· 确保 C 层成功创建了定时器并返回了有效 session。
· 如果 session 为 nil/0,说明创建失败,直接断言失败。
· local co = co_create_for_timeout(func, ti)
· 在 Lua 层创建一个协程,把用户回调 func 封装进去。
· 注意:这里只是创建协程,并没有立即 resume。
· 这个协程会一直挂起,直到定时器到期后收到 PTYPE_RESPONSE 消息才被唤醒。
· assert(session_id_coroutine[session] == nil)
· 检查这个 session 还没有被别的协程占用。
· 正常情况下每个 session 唯一,避免覆盖。
· session_id_coroutine[session] = co
· 关键映射:session -> 协程。
· C 层到期后只知道 handle + session,Lua 层靠这个表找回协程。
· 这是 C 层与 Lua 层解耦的关键。
\skynet\skynet-src\skynet_server.c
static const char *
cmd_timeout(struct skynet_context * context, const char * param) {
char * session_ptr = NULL;
int ti = strtol(param, &session_ptr, 10);
int session = skynet_context_newsession(context);
skynet_timeout(context->handle, ti, session);
sprintf(context->result, "%d", session);
return context->result;
}
· static const char * cmd_timeout(...)
· C 层处理 Lua 发来的 "TIMEOUT" 命令。
· context 是当前服务上下文。
· param 是 Lua 传来的参数字符串。
· char * session_ptr = NULL;
· 给 strtol 用的结束指针,后面没使用。
· int ti = strtol(param, &session_ptr, 10);
· 把字符串参数解析成整数 ti,即延迟 tick 数。
· int session = skynet_context_newsession(context);
· 从当前服务上下文分配一个新的 session。
· session 用于匹配请求和响应,定时器到期后会带着这个 session 回来。
· skynet_timeout(context->handle, ti, session);
· 真正注册定时器。
· context->handle 是目标服务地址。
· ti 是延迟。
· session 是之后响应消息要携带的 session。
· sprintf(context->result, "%d", session);
· 把 session 写进上下文的命令结果缓冲区。
· Lua 层 cintcommand 会把它转成整数返回。
· return context->result;
· 返回结果字符串给命令调用方。
\skynet\skynet-src\skynet_timer.c
int
skynet_timeout(uint32_t handle, int time, int session) {
if (time <= 0) {
struct skynet_message message;
message.source = 0;
message.session = session;
message.data = NULL;
message.sz = (size_t)PTYPE_RESPONSE << MESSAGE_TYPE_SHIFT;
if (skynet_context_push(handle, &message)) {
return -1;
}
} else {
struct timer_event event;
event.handle = handle;
event.session = session;
timer_add(TI, &event, sizeof(event), time);
}
return session;
}
· int skynet_timeout(uint32_t handle, int time, int session)
· handle:目标服务地址。
· time:延迟多少 tick。
· session:响应匹配号。
· 返回值:成功返回 session,失败返回 -1。
· if (time <= 0) {
· 如果延迟小于等于 0,不放入时间轮,直接投递一条超时响应消息。
· 注意:不是直接执行 Lua 回调,而是走服务消息队列,保证服务消息串行处理。
· struct skynet_message message;
· 构造一条 skynet 消息。
· message.source = 0;
· 来源设为 0,表示系统/定时器来源,不是某个普通服务。
· message.session = session;
· 携带 session,Lua 层靠它找到协程。
· message.data = NULL;
· 定时器响应没有数据体。
· message.sz = (size_t)PTYPE_RESPONSE << MESSAGE_TYPE_SHIFT;
· 把消息类型编码进 sz 高位。
· PTYPE_RESPONSE 是响应类型,Lua 层 raw_dispatch_message 中 prototype == 1 会进入响应分支。
· if (skynet_context_push(handle, &message)) { return -1; }
· 把消息投递到目标服务的私有消息队列。
· 失败返回 -1。
· } else {
· 如果 time > 0,正常挂入时间轮。
· struct timer_event event;
· 定时器事件,只有两个字段:handle 和 session。
· event.handle = handle;
· 到期后要投递给哪个服务。
· event.session = session;
· 到期后响应消息携带哪个 session。
· timer_add(TI, &event, sizeof(event), time);
· 把事件加入全局时间轮 TI。
· sizeof(event) 表示事件大小,后面会和 timer_node 一起连续分配。
· return session;
· 返回 session 给上层。
\skynet\skynet-src\skynet_server.c
int
skynet_context_push(uint32_t handle, struct skynet_message *message) {
struct skynet_context * ctx = skynet_handle_grab(handle);
if (ctx == NULL) {
return -1;
}
skynet_mq_push(ctx->queue, message);
skynet_context_release(ctx);
return 0;
}
\skynet\skynet-src\skynet_handle.c
struct skynet_context *
skynet_handle_grab(uint32_t handle) {
struct handle_storage *s = H;
struct skynet_context * result = NULL;
handle_rlock(s);
uint32_t hash = handle & (s->slot_size-1);
struct skynet_context * ctx = s->slot[hash];
if (ctx && skynet_context_handle(ctx) == handle) {
result = ctx;
skynet_context_grab(result);
}
handle_runlock(s);
return result;
}
上面这两个方法不在本文的讨论范围,故没有文字解释。
\skynet\skynet-src\skynet_timer.c
static void
timer_add(struct timer *T,void *arg,size_t sz,int time) {
struct timer_node *node = (struct timer_node *)skynet_malloc(sizeof(*node)+sz);
memcpy(node+1,arg,sz);
SPIN_LOCK(T);
node->expire=time+T->time;
add_node(T,node);
SPIN_UNLOCK(T);
}
· static void timer_add(struct timer *T, void *arg, size_t sz, int time)
· T:全局时间轮实例。
· arg:timer_event。
· sz:timer_event 大小。
· time:相对延迟 tick 数。
· struct timer_node *node = (struct timer_node *)skynet_malloc(sizeof(*node)+sz);
· 一次性分配 timer_node + timer_event。
· node 在前,event 紧跟在 node+1 后面。
· 好处:减少内存碎片,释放时一次 skynet_free(node) 即可。
· memcpy(node+1,arg,sz);
· 把 timer_event 拷贝到 node 后面。
· SPIN_LOCK(T);
· 加自旋锁,保护时间轮链表。
· 因为 timer 线程会推进时间轮,其他 worker 线程也可能调用 skynet.timeout 注册新定时器。
· node->expire=time+T->time;
· 计算绝对到期 tick:
· T->time 是当前逻辑 tick。
· time 是相对延迟。
· 例如当前 T->time=100,time=10,则 expire=110。
· add_node(T,node);
· 根据 expire 把节点挂到时间轮对应层级的槽位链表里。
· SPIN_UNLOCK(T);
· 解锁。
3.C层把定时器节点按到期时间挂入时间轮的对应槽位;
\skynet\skynet-src\skynet_timer.c
static void
add_node(struct timer *T,struct timer_node *node) {
uint32_t time=node->expire;
uint32_t current_time=T->time;
if ((time|TIME_NEAR_MASK)==(current_time|TIME_NEAR_MASK)) {
link(&T->near[time&TIME_NEAR_MASK],node);
} else {
int i;
uint32_t mask=TIME_NEAR << TIME_LEVEL_SHIFT;
for (i=0;i<3;i++) {
if ((time|(mask-1))==(current_time|(mask-1))) {
break;
}
mask <<= TIME_LEVEL_SHIFT;
}
link(&T->t[i][((time>>(TIME_NEAR_SHIFT + i*TIME_LEVEL_SHIFT)) & TIME_LEVEL_MASK)],node);
}
}
· uint32_t time = node->expire;
· 节点的绝对到期 tick。
· uint32_t current_time = T->time;
· 当前逻辑 tick。
· if ((time|TIME_NEAR_MASK)==(current_time|TIME_NEAR_MASK))
· 判断到期时间是否落在当前 near 层覆盖范围内。
· TIME_NEAR_MASK = 255,near 层有 256 个槽。
· 条件本质是:time 和 current_time 的高位相同,即到期时间在最近 256 个 tick 内。
· 如果成立,直接放 near 层。
· link(&T->near[time&TIME_NEAR_MASK],node);
· 放到 near 层对应槽。
· time & 255 取低 8 位作为槽索引。
· } else {
· 如果超过 near 层范围,放到更高层 t[0..3]。
· int i;
· 层级索引。
· uint32_t mask = TIME_NEAR << TIME_LEVEL_SHIFT;
· 初始 mask 对应 t[0] 的覆盖范围。
· TIME_NEAR=256,TIME_LEVEL_SHIFT=6,所以 mask=256<<6=16384。
· for (i=0;i<3;i++) {
· 从 t[0] 到 t[2] 查找合适的层级。
· 如果都不满足,最终 i=3,落到最高层 t[3]。
· if ((time|(mask-1))==(current_time|(mask-1))) { break; }
· 判断 time 和 current_time 在 mask 范围内高位是否相同。
· 找到第一个能覆盖到期时间的层级就跳出。
· mask <<= TIME_LEVEL_SHIFT;
· 扩大覆盖范围,检查更高层。
· link(&T->t[i][((time>>(TIME_NEAR_SHIFT + i*TIME_LEVEL_SHIFT)) & TIME_LEVEL_MASK)],node);
· 计算 t[i] 层的槽索引并挂入。
\skynet\skynet-src\skynet_timer.c
static inline void
link(struct link_list *list,struct timer_node *node) {
list->tail->next = node;
list->tail = node;
node->next=0;
}
· list->tail->next = node;
· 把新节点挂到链表尾部。
· list->tail = node;
· 更新尾指针。
· node->next=0;
· 新节点是最后一个,next 置空。
这是 O(1) 尾插。同一槽位里的定时器节点按插入顺序串成链表。
timer线程
\skynet\skynet-src\skynet_start.c
static void *
thread_timer(void *p) {
struct monitor * m = p;
skynet_initthread(THREAD_TIMER);
skynet_handle_register_thread();
for (;;) {
skynet_updatetime();
skynet_socket_updatetime();
CHECK_ABORT
wakeup(m,m->count-1);
usleep(2500);
if (SIG) {
signal_hup();
SIG = 0;
}
}
// wakeup socket thread
skynet_socket_exit();
// wakeup all worker thread
pthread_mutex_lock(&m->mutex);
m->quit = 1;
pthread_cond_broadcast(&m->cond);
pthread_mutex_unlock(&m->mutex);
return NULL;
}
· struct monitor * m = p;
· 线程参数,包含全局监控/调度信息。
· skynet_initthread(THREAD_TIMER);
· 初始化当前线程的线程局部信息,标记为 timer 线程。
· skynet_handle_register_thread();
· 向 handle 模块注册当前线程。
· for (;;) {
· timer 线程主循环。
· skynet_updatetime();
· 推进时间轮。
· 这是定时器核心:根据真实时间差推进逻辑 tick,触发到期定时器。
· skynet_socket_updatetime();
· 更新 socket 超时相关时间。
· 虽然不属于定时器核心,但 timer 线程顺带驱动。
· CHECK_ABORT
· 宏,检查 skynet 是否准备退出。
· wakeup(m,m->count-1);
· 唤醒 worker 线程去消费消息队列。
· timer 线程只投递消息,不执行 Lua 回调。
· usleep(2500);
· 每 2.5ms 轮询一次。
· 注意:时间轮 tick 默认 10ms,但 timer 线程不是每 10ms 才醒,而是每 2.5ms 检查一次,skynet_updatetime 内部按真实时间差推进多个 tick。
· if (SIG) { signal_hup(); SIG = 0; }
· 处理挂断信号。
· 退出时:
· skynet_socket_exit();
· 通知 socket 线程退出。
· pthread_mutex_lock(&m->mutex);
· m->quit = 1;
· 设置退出标志。
· pthread_cond_broadcast(&m->cond);
· 唤醒所有 worker 线程。
· pthread_mutex_unlock(&m->mutex);
· 解锁。
· return NULL;
· 线程结束。
4.timer线程每10ms推进一次时间轮;
\skynet\skynet-src\skynet_timer.c
void
skynet_updatetime(void) {
uint64_t cp = gettime();
if(cp < TI->current_point) {
skynet_error(NULL, "time diff error: change from %lld to %lld", cp, TI->current_point);
TI->current_point = cp;
} else if (cp != TI->current_point) {
uint32_t diff = (uint32_t)(cp - TI->current_point);
TI->current_point = cp;
TI->current += diff;
int i;
for (i=0;i<diff;i++) {
timer_update(TI);
}
}
}
· uint64_t cp = gettime();
· 获取当前真实时间。
· skynet 中 gettime() 通常返回以 10ms 为单位的时间。
· if(cp < TI->current_point) {
· 检测时间回拨。
· 如果当前时间小于上次记录时间,说明系统时间被调整。
· skynet_error(...);
· 打印错误日志。
· TI->current_point = cp;
· 重置当前真实时间点,避免时间轮倒退。
· } else if (cp != TI->current_point) {
· 如果时间前进了。
· uint32_t diff = (uint32_t)(cp - TI->current_point);
· 计算经过了多少个 tick。
· 正常每 2.5ms 检查一次,diff 通常是 0 或 1。
· TI->current_point = cp;
· 更新真实时间点。
· TI->current += diff;
· 更新逻辑当前时间。
· int i;
· for (i=0;i<diff;i++) { timer_update(TI); }
· 每经过一个 tick 就推进一次时间轮。
· 如果一次检查发现过了多个 tick,会循环补齐,保证每个 tick 都处理。
\skynet\skynet-src\skynet_timer.c
static void
timer_update(struct timer *T) {
SPIN_LOCK(T);
// try to dispatch timeout 0 (rare condition)
timer_execute(T);
// shift time first, and then dispatch timer message
timer_shift(T);
timer_execute(T);
SPIN_UNLOCK(T);
}
· SPIN_LOCK(T);
· 锁住时间轮。
· timer_execute(T);
· 先执行当前 tick 的 near 槽。
· 注释说处理 timeout 0 的罕见情况。
· timer_shift(T);
· 推进时间轮一格:
· T->time++
· 必要时把上层槽的节点降层到 near 或更低层。
· timer_execute(T);
· 再执行一次当前 tick 的 near 槽。
· 因为 timer_shift 可能刚把上层节点移动到当前 near 槽,需要立即处理。
· SPIN_UNLOCK(T);
· 解锁。
顺序很重要:先执行当前槽 -> 推进时间轮 -> 再执行新槽。
5.定时器到期后,timer线程把一条超时信息投递到目标服务的私有队列;
\skynet\skynet-src\skynet_timer.c
static inline void
timer_execute(struct timer *T) {
int idx = T->time & TIME_NEAR_MASK;
while (T->near[idx].head.next) {
struct timer_node *current = link_clear(&T->near[idx]);
SPIN_UNLOCK(T);
// dispatch_list don't need lock T
dispatch_list(current);
SPIN_LOCK(T);
}
}
· int idx = T->time & TIME_NEAR_MASK;
· 当前 tick 对应 near 层槽索引。
· TIME_NEAR_MASK=255,即 T->time % 256。
· while (T->near[idx].head.next) {
· 只要当前槽链表非空就处理。
· 用 head.next 判断是否有节点。
· struct timer_node *current = link_clear(&T->near[idx]);
· 把当前槽整条链表摘下来,返回链表头。
· link_clear 会清空槽位,后续新插入的定时器不会混进当前链表。
· SPIN_UNLOCK(T);
· 先解锁,再投递消息。
· 因为 dispatch_list 会调用 skynet_context_push,可能比较耗时,不应长时间持有全局时间轮锁。
· dispatch_list(current);
· 遍历链表,对每个节点:
· 取出 timer_event。
· 构造 PTYPE_RESPONSE 消息。
· skynet_context_push(event->handle, &message) 投递到目标服务。
· 释放 timer_node。
· 这一步只是投递消息,不执行 Lua 回调。
· SPIN_LOCK(T);
· 重新加锁,继续检查当前槽是否又有新节点。
\skynet\skynet-src\skynet_timer.c
static void
timer_shift(struct timer *T) {
int mask = TIME_NEAR;
uint32_t ct = ++T->time;
if (ct == 0) {
move_list(T, 3, 0);
} else {
uint32_t time = ct >> TIME_NEAR_SHIFT;
int i=0;
while ((ct & (mask-1))==0) {
int idx=time & TIME_LEVEL_MASK;
if (idx!=0) {
move_list(T, i, idx);
break;
}
mask <<= TIME_LEVEL_SHIFT;
time >>= TIME_LEVEL_SHIFT;
++i;
}
}
}3
· int mask = TIME_NEAR;
· 初始 mask 为 256。
· uint32_t ct = ++T->time;
· 逻辑 tick 自增,并保存新值。
· if (ct == 0) {
· 32 位 tick 回绕。
· move_list(T, 3, 0);
· 把最高层 t[3][0] 的节点重新分配到时间轮。
· 处理回绕后的定时器。
· } else {
· 正常推进。
· uint32_t time = ct >> TIME_NEAR_SHIFT;
· 去掉 near 层低 8 位,准备检查上层槽。
· int i=0;
· 从 t[0] 开始。
· while ((ct & (mask-1))==0) {
· 判断当前 tick 是否跨过某个层级的边界。
· 初始 mask=256,即 ct 是 256 的倍数时,near 层转完一圈,需要把 t[0] 对应槽降层。
· int idx = time & TIME_LEVEL_MASK;
· 取当前层槽索引。
· TIME_LEVEL_MASK=63。
· if (idx != 0) {
· 如果该槽非空索引。
· move_list(T, i, idx);
· 把 t[i][idx] 链表摘下,重新 add_node 到时间轮。
· 因为当前 T->time 已经推进,这些节点可能落到 near 层或更低层。
· break;
· 一次只降一层,处理完跳出。
· mask <<= TIME_LEVEL_SHIFT;
· 扩大 mask,检查更高层。
· time >>= TIME_LEVEL_SHIFT;
· 右移,准备取更高层槽索引。
· ++i;
· 层级加一。
\skynet\lualib\skynet.lua
local function raw_dispatch_message(prototype, msg, sz, session, source)
-- skynet.PTYPE_RESPONSE = 1, read skynet.h
if prototype == 1 then
local co = session_id_coroutine[session]
if co == "BREAK" then
session_id_coroutine[session] = nil
elseif co == nil then
unknown_response(session, source, msg, sz)
else
local tag = session_coroutine_tracetag[co]
if tag then c.trace(tag, "resume") end
session_id_coroutine[session] = nil
suspend(co, coroutine_resume(co, true, msg, sz, session))
end
else
-- 普通请求分支,和定时器无直接关系
end
end
从设置超时定时器到触发定时器流程
Lua: skynet.timeout → auxtimeout → cintcommand("TIMEOUT")
│
C: ▼
skynet_command → cmd_timeout → skynet_context_newsession
→ skynet_timeout [skynet_timer.c]
├─ ti<=0: skynet_context_push 立即投递
└─ ti>0 : timer_add → add_node 放入时间轮
Lua: co_create_for_timeout → co_create(协程挂起)
Lua: session_id_coroutine[session] = co;skynet.timeout 返回
......时间推进......
C: skynet_updatetime(每 2.5ms 轮询)→ timer_update
├─ timer_execute → dispatch_list → skynet_context_push(PTYPE_RESPONSE)
├─ timer_shift → move_list → add_node(级联降层)
└─ timer_execute
Lua: raw_dispatch_message 收到 PTYPE_RESPONSE
└─ coroutine_resume(co) → func() 执行
总结:
1. Lua 调 skynet.timeout(ti, func)。
2. Lua 通过 C 命令 TIMEOUT 进入 C 层,分配 session,并注册定时器。
3. 如果 ti<=0,C 层立即投递一条 PTYPE_RESPONSE 消息;否则挂入分层时间轮。
4. Lua 层创建协程,并保存 session -> coroutine 映射。
5. timer 线程每 2.5ms 轮询,按真实时间差推进逻辑 tick。
6. 每个 tick:
· 执行当前 near 槽到期节点;
· 推进时间轮,必要时把上层槽节点降层;
· 再执行新 near 槽。
7. 到期节点被转换成 PTYPE_RESPONSE 消息,投递到目标服务消息队列。
8. worker 线程处理该服务消息时,进入 raw_dispatch_message 的响应分支。
9. 根据 session 找到协程,coroutine_resume 唤醒协程。
10. 协程执行用户传入的 func。
性能注意点
结合源码,乱用定时器主要会带来这些开销:
· timer_add 需要拿全局时间轮自旋锁,频繁注册定时器会竞争锁。
· 大量短定时器会集中在 near 槽链表,到期时 dispatch_list 一次遍历投递大量消息。
· 每个定时器到期都会产生一条服务消息,增加目标服务消息队列压力和 worker 调度开销。
· skynet_updatetime 如果一次发现 diff 很大,会循环 diff 次 timer_update,可能阻塞 timer 线程。
· Lua 层 session_id_coroutine 会为每个未到期定时器保存一个协程引用,大量长期定时器会增加 Lua 内存和 GC 压力。
· 回调函数在 worker 线程执行,如果回调耗时,会占用 worker,影响服务处理其他消息。
所以业务上应避免:
· 高频创建极短定时器;
· 在定时器回调里做重计算或阻塞操作;
· 大量无意义的定时器;
· 用定时器轮询代替事件驱动。
上面说了Skynet的定时器是一个分层时间轮,下面具体讲讲这个时间轮: 结构如下:
c
\skynet\skynet-src\skynet_timer.c
#define TIME_NEAR_SHIFT 8
#define TIME_NEAR (1 << TIME_NEAR_SHIFT)
#define TIME_LEVEL_SHIFT 6
#define TIME_LEVEL (1 << TIME_LEVEL_SHIFT)
struct timer {
struct link_list near[TIME_NEAR];
struct link_list t[4][TIME_LEVEL];
struct spinlock lock;
uint32_t time;
uint32_t starttime;
uint64_t current;
uint64_t current_point;
};
struct timer_node {
struct timer_node *next;
uint32_t expire;
};
struct link_list {
struct timer_node head;
struct timer_node *tail;
};
默认一个 tick 是 10ms,所以:
· near 层有 256 个槽,覆盖 256 × 10ms = 2.56 秒; · t0 覆盖约 163.84 秒; · t1 覆盖约 2.91 小时; · t2 覆盖约 7.77 天; · t3 覆盖约 497 天。
每个槽位挂一条链表,链表里是到期时间落在同一个 tick 的定时器节点。定时器节点 timer_node 和事件 timer_event 通常是一次连续分配的,event 紧跟在 node 后面,减少内存碎片。
timer_event 里保存的是:
c
struct timer_event {
uint32_t handle; // 目标服务地址
int session; // 超时请求的 session
};
注意:C 层不保存 Lua 函数,只保存 handle + session。Lua 函数保存在 Lua 层的 session_id_coroutine 表里。这是 C 层和 Lua 层解耦的关键。
其他常见定时器
· 最小堆:Redis 常用,插入 O(logN),取最近到期 O(1);
· 红黑树:Nginx 常用,插入删除 O(logN);
· 时间轮:Skynet 使用,插入 O(1),触发时批量处理。
Skynet 选择时间轮,核心原因是游戏服务端定时器数量多、触发频繁。时间轮插入是 O(1),分层设计又避免了大量节点频繁搬移。一个很远的定时器可能先放在高层,等时间接近了再被重新分配到低层,不需要每次 tick 都调整。
注意:Skynet 原生定时器不可取消 一旦设置,不能直接撤销。
这篇文章就写到这,感谢阅读。