《从 skynet.timeout 到回调执行:Skynet 分层时间轮源码剖析》

作为一名程序员,虽然在写业务逻辑代码时,定时器代码实现好了,然而,乱用定时器也会导致性能降低,所以为了项目的性能考虑,需要我们去了解一下定时器的实现和使用注意。

要了解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 原生定时器不可取消 一旦设置,不能直接撤销。

这篇文章就写到这,感谢阅读。

相关推荐
鶴哥只手遮天30 分钟前
《Redis 8.10.3 深度剖析》系列 · 第 1 章·全景与源码地图:版本谱系、构建体系、全局状态与启动链路
redis·后端·开源
一条Go35 分钟前
四个进程,一把锁:Alioth 的告警评测为什么不需要消息队列
后端·设计模式·架构
用户1996143559961 小时前
插件是怎么被安全装进系统的
后端
愚农搬码1 小时前
自研表单引擎、开源组件二开和商业产品,企业应该如何选择?
后端
用户298698530141 小时前
Python 自动化处理 Word 页眉页脚的几种典型场景
后端·python·api
愚农搬码2 小时前
一款企业级电子表单系统应该具备哪些功能?
后端
Rain的Java大神之路2 小时前
国家数据集团校招、社招都开了:总部雄安,这些技术岗值得认真看看
java·后端·面试
XW2 小时前
Agent 上下文管理实现详解:从窗口工程到长期记忆
后端
Moment2 小时前
为什么这么多 Agent Harness 都选择 TypeScript
前端·后端·面试