以下是 timer 线程的核心流程代码。Skynet 的 Timer 模块是通过一个单独的线程 (thread_timer) 来定期更新定时器的状态。每隔 2500 微秒(2.5ms)更新一次定时器的状态。ps:为什么是 2500?
static void *
thread_timer(void *p) {
struct monitor * m = p;
skynet_initthread(THREAD_TIMER);
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;
}
每次 skynet_updatetime() 被调用时,它会检查当前的系统时间与定时器的当前时间戳,并更新所有定时任务的超时状态。通过 usleep(2500) 控制定时器更新的频率。
定时器的时间更新分为两部分:时间差计算和超时事件的分发。skynet_updatetime() 中通过对当前时间戳 (cp) 与定时器的上次时间戳进行对比,计算出时间差。然后通过 timer_update(TI) 分发超时事件。
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
在定时器更新过程中,每当有事件超时时,会执行以下操作:
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);
}
timer_update 主要做了两件事:
timer_shift(T):更新时间戳,并将过期的定时任务从近到远进行“移位”处理。
timer_execute 通过遍历 near[idx] 列表中的任务,调用 dispatch_list 来处理这些超时的事件。通过 SPIN_LOCK 和 SPIN_UNLOCK 确保对定时器的并发访问安全。
static inline void
timer_execute(struct timer *T) {
// TIME_NEAR_MASK 就是 255,与运算求出 T->time 所在 near 数据的 index
int idx = T->time & TIME_NEAR_MASK;
// 直接遍历这个时刻的链表,dispatch_list所有 event
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);
}
}
最终其实就是派发一个消息到 对应的 skynet_context。
static inline void
dispatch_list(struct timer_node *current) {
do {
struct timer_event * event = (struct timer_event *)(current+1);
struct skynet_message message;
message.source = 0;
message.session = event->session;
message.data = NULL;
message.sz = (size_t)PTYPE_RESPONSE << MESSAGE_TYPE_SHIFT;
skynet_context_push(event->handle, &message);
struct timer_node * temp = current;
current=current->next;
skynet_free(temp);
} while (current);
}
新的定时任务通过 timer_add 函数加入到定时器中: 每个定时任务的过期时间 expire 会被计算并添加到相应的定时器队列。通过 add_node 将任务插入到正确的位置。
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);
}
#define TIME_NEAR_SHIFT 8
#define TIME_NEAR (1 << TIME_NEAR_SHIFT) // => 2^8 = 256
#define TIME_LEVEL_SHIFT 6
#define TIME_LEVEL (1 << TIME_LEVEL_SHIFT) // => 2^6 = 64
#define TIME_NEAR_MASK (TIME_NEAR-1) // => 255
#define TIME_LEVEL_MASK (TIME_LEVEL-1) // => 63
t[4][TIME_LEVEL]
就是 4 层远距离时间轮,每层各 64 槽。TIME_NEAR_MASK = 255
, TIME_LEVEL_MASK = 63
,用于做位运算,快速定位某个槽。timer_event
struct timer_event {
uint32_t handle;
int session;
};
这个结构往往附加在定时节点后面,包含要分发的消息信息。
timer_node
struct timer_node {
struct timer_node *next;
uint32_t expire; // 过期时间(相对timer->time)
};
link_list
struct link_list {
struct timer_node head;
struct timer_node *tail;
};
head.next
才是第一个真实节点)。在 Skynet Timer 中,这样的 link_list
用于存放同一时间槽下的多个定时任务。
struct timer
struct timer {
struct link_list near[TIME_NEAR]; // 数组大小256 (TIME_NEAR)
struct link_list t[4][TIME_LEVEL]; // 4层, 每层64个槽
struct spinlock lock; // 自旋锁
uint32_t time; // 当前时间(相对)
uint32_t starttime; // 启动时间
uint64_t current; // 记录累计时间(详见skynet_updatetime)
uint64_t current_point; // 上一次更新时间, 用于计算时间差
};
[TIME_NEAR]
),管理距离当前时间不远的任务(精细粒度)。add_node
)可以在别的线程调用,而执行定时器(Timer线程)也会操作同一个结构。多级时间轮(Multilevel Timer Wheel)可以简单理解为“多个环形数组”,每个数组的槽代表一段时间区间。
t[0..3]
)表示越来越大的时间跨度(每层64个槽, 4层)当time每+1,就相当于“最底层时间轮”转动一步 => 过期的任务就会被执行。
若某任务过期时间很远,放到更高层中 => 随着time增加到一定阈值时再“移位”到更低层,直到进入near后就会很快被执行。
add_node
函数:插入定时节点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)) {
// 若这个任务与当前time同属于"低8位"范围 => 放入 near数组
link(&T->near[time & TIME_NEAR_MASK], node);
} else {
int i;
uint32_t mask = TIME_NEAR << TIME_LEVEL_SHIFT;
// mask = 256 << 6 = 16384
for (i=0; i<3; i++) {
// 用mask做位运算判断 => 如果满足 => break
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);
}
}
比较过期时间 node->expire
与当前 T->time
,判断它们在位运算后是否处于同一“近距离”范围 (time|TIME_NEAR_MASK
)
near[索引]
t[i]
)for (i=0; i<3; i++) 用位运算判断该任务适合放到第几层
mask
初始值 = TIME_NEAR<(=256<<6=16384)
(time | (mask-1)) == (current_time | (mask-1))
来判断任务和当前时间是不是落在同一个对齐区间最终 link(...)
(time >> (TIME_NEAR_SHIFT + i*TIME_LEVEL_SHIFT)) & TIME_LEVEL_MASK
计算得到具体槽索引node
插到 t[i][槽]
或 near[槽]
TIME_NEAR_MASK
=255 => “低8位”掩码
(time | TIME_NEAR_MASK) == (current_time | TIME_NEAR_MASK)
用于判断 time
与 current_time
在低8位里是否相同(即它们只在 0~255 范围内相差不大)。
对更高层时:
这里 (mask-1)
例如 16384-1=16383 (二进制 111111111111111) => 检查更高位范围
uint32_t mask = TIME_NEAR << TIME_LEVEL_SHIFT; // 256 <<6=16384
for (i=0;i<3;i++){
if ((time|(mask-1))==(current_time|(mask-1))) break;
mask <<= TIME_LEVEL_SHIFT;
}
add_node
仅做常数次循环(最多3次) + 链表尾插 => O(1)3.2 timer_execute
:取出即将到期的节点并执行在 Skynet Timer 中,最底层 near 数组的当前槽(由 T->time & TIME_NEAR_MASK
) 表示这个刻度要执行的任务。
通常过程(在 timer_update
)是:
static 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]);
// link_clear 把这一槽的链表清空并返回头结点
SPIN_UNLOCK(T);
dispatch_list(current);
SPIN_LOCK(T);
}
}
(T->time & TIME_NEAR_MASK)
=> 定位 near 的哪个槽link_clear
把这个槽的链表全部取出(要执行的定时节点),然后dispatch_list 分发执行dispatch_list 里,会根据 timer_event
做消息推送或回调(比如 skynet_context_push
)。
static inline void
dispatch_list(struct timer_node *current) {
do {
struct timer_event * event = (struct timer_event *)(current+1);
struct skynet_message message;
message.source = 0;
message.session = event->session;
message.data = NULL;
message.sz = (size_t)PTYPE_RESPONSE << MESSAGE_TYPE_SHIFT;
skynet_context_push(event->handle, &message);
struct timer_node * temp = current;
current=current->next;
skynet_free(temp);
} while (current);
}
3.3 timer_shift
:时间轮中“移位” 处理
除了 timer_execute
还需要把更高层的任务逐层“移位”下来。
t[i]
某槽里的节点移到更底层(near层), 使之逐渐接近执行timer_shift
所做的事。效果: 远期任务先放高层, 等时间越来越近时才搬运到下一级, 直到near再被timer_execute
拿走执行。
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;
}
}
}
void
skynet_timer_init(void) {
TI = timer_create_timer();
uint32_t current = 0;
systime(&TI->starttime, ¤t);
TI->current = current;
TI->current_point = gettime();
}
uint64_t gettime() {
uint64_t t;
struct timespec ti;
clock_gettime(CLOCK_MONOTONIC, &ti);
t = (uint64_t)ti.tv_sec * 100; // 将秒转换为 10毫秒为单位
t += ti.tv_nsec / 10000000; // 将纳秒转换为 10毫秒为单位
return t;
}
T->time
% 256 == x, 表示 near[x] 这个槽是当前刻度也就是说 多级 => 可以涵盖相当大的时间跨度(2^8 * 2^(6*4)) * 10ms。
Skynet Timer 通过多级时间轮加上位运算的巧妙组合,使得定时任务的插入与执行都非常高效。近距离(小时间差)和远距离(大时间差)任务分层管理,既照顾了频繁触发的短周期任务,又能处理周期较大的远期任务,并且通过简单的链表操作实现 O(1) 的插入/移除。如果对性能要求不高还可以使用最小堆等,不过时间复杂度 就是 logN 了。