继续按照skynet.start中的顺序分析。分析skynet定时器。启动各个线程都会做类似的初始化。
定时器分析
这里要做一个前置理解,定时器,是只需要关心最近即将到来的任务,而不用关心距离现在比较远的任务,例如
1.你注册了100个定时任务,定时任务按照时间排序后
2.A任务2s后触发,B任务3h后触发,C任务h后触发…
3.当刷新时间的时候,需要去拿到即将到来的任务而需要关心之后的任务,这里就是只需要关心A任务,也就是你不需要去遍历所有任务列表拿到即将发生的任务,因为B之后的任务包括B,时间间隔太远了都不需要去关心。
所以定时器的设计需要:
1.查询快
2.做好排序
3.删除做完的任务之后不影响之前结构
常见的设计例如使用最小堆,时间轮,红黑树,而skynet使用的是时间轮。
时间轮的设计就类似于我们生活中的钟表,大概说就是,秒针走一圈,分针走一格,分针走一圈,时针走一格。
运用到程序中,大概就是,
1.我们只关心秒针的任务。
2.秒针的一圈任务执行完了之后,拿分针一格的任务分配到秒针各秒中。这样就我们每次都只会从秒针的任务中拿到需要执行的任务,和第1点一致。
3.分针的一圈任务执行完了之后,拿时针一格的任务分配到分针各分中。
先看定时器结构
#define TIME_NEAR_SHIFT 8 // 临近时间转移量
#define TIME_NEAR (1 << TIME_NEAR_SHIFT) // 临近时间量 0x10000
#define TIME_LEVEL_SHIFT 6 // 别的时间等级转移量
#define TIME_LEVEL (1 << TIME_LEVEL_SHIFT) // 时间等级 0x1100
#define TIME_NEAR_MASK (TIME_NEAR-1) // 临近时间掩码 0x1111
#define TIME_LEVEL_MASK (TIME_LEVEL-1) // 别的时间等级掩码 0x1011
struct timer_event {
uint32_t handle; //即是设置定时器的来源,又是超时消息发送的目标
int session; //session,一个增ID,溢出了从1开始,所以不要设时间很长的timer
};
// 时间节点链表
struct timer_node {
struct timer_node *next; // 下一个节点地址
uint32_t expire; // 到期时间
};
// 定时器任务链表,
// 链表每个节点是一个时间节点链表
// 这里一定要清楚,定时器任务链表上每个节点是一个时间节点链表
// 即同一时间的任务链表
struct link_list {
struct timer_node head; // 头节点
struct timer_node *tail; // 尾节点
};
struct timer {
// (临近时间)8 + 4*(另外等级)6 = 32,刚好32位
struct link_list near[TIME_NEAR]; // 临近时间的定时器任务链表,也就是我上面说的秒针
struct link_list t[4][TIME_LEVEL]; // 剩下的四个等级的任务链表
struct spinlock lock; // 自旋锁
uint32_t time; // 服务器经过的的tick数,每10毫秒tick一次
uint32_t starttime; // 程序启动时间戳
uint64_t current; // 启动到现在的耗时,精度10毫秒级
uint64_t current_point; // 当前时间,精度10毫秒级
};
定时器初始化
skynet_start中调用 skynet_timer_init初始化,在我的《从头开始读skynet源码(1)》中有写。
// skynet_start中调用 skynet_timer_init初始化
void
skynet_timer_init(void) {
TI = timer_create_timer();
uint32_t current = 0;
systime(&TI->starttime, ¤t);
TI->current = current;
TI->current_point = gettime();
}
static struct timer *
timer_create_timer() {
struct timer *r=(struct timer *)skynet_malloc(sizeof(struct timer));
memset(r,0,sizeof(*r));
int i,j;
// 重置临近时间定时器任务链表
for (i=0;i<TIME_NEAR;i++) {
link_clear(