skynet 源码阅读 -- timer 的实现原理

1. Timer 驱动的核心流程

1.1 Timer 线程驱动

以下是 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) 控制定时器更新的频率。

1.2 时间更新和超时事件的分发

定时器的时间更新分为两部分:时间差计算和超时事件的分发。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

1.3 超时事件分发

在定时器更新过程中,每当有事件超时时,会执行以下操作:

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 主要做了两件事:

  • SPIN_LOCK(T);  获取自旋锁
  •  timer_execute(T):负责执行当前定时器队列中的事件,并通过 dispatch_list(current) 将这些事件分发给相应的处理线程。
  • timer_shift(T):更新时间戳,并将过期的定时任务从近到远进行“移位”处理。

1.4 事件执行与分发

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);
	}
}

1.5 任务 dispatch

最终其实就是派发一个消息到 对应的 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);
}

1.6 定时任务添加

新的定时任务通过 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);
}

2. 算法与数据结构的实现

2.1 数据结构定义

关键常量
#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
  • TIME_NEAR:定时器中“近距离”时间轮的大小为 256。换言之,这个“near”数组有 256 个槽,每个槽代表一个时间刻度。
  • TIME_LEVEL:定时器“远距离”时间轮的大小为 64。后面会看到数组 t[4][TIME_LEVEL] 就是 4 层远距离时间轮,每层各 64 槽。
  • MASKTIME_NEAR_MASK = 255, TIME_LEVEL_MASK = 63,用于做位运算,快速定位某个槽。
timer_event
struct timer_event {
    uint32_t handle;
    int session;
};
  • handle:表示定时任务处理的目标(如服务ID)。
  • session:该定时事件对应的会话或序号,用来区分不同事件。

这个结构往往附加在定时节点后面,包含要分发的消息信息。

timer_node
struct timer_node {
    struct timer_node *next;
    uint32_t expire; // 过期时间(相对timer->time)
};
  • next:链表指针,用于串联多个定时任务。
  • expire:表示这个任务会在什么时间点过期(触发)。这个“时间”是一个递增计数值,而非绝对系统时间。
link_list
struct link_list {
    struct timer_node head;
    struct timer_node *tail;
};
  • head:链表头哨兵节点(通常不存实际任务,head.next才是第一个真实节点)。
  • tail:指向链表中最后一个节点。方便快速插入到尾部。

在 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;                 // 上一次更新时间, 用于计算时间差
};
  • near:近距离时间轮,有 256 个槽 ([TIME_NEAR]),管理距离当前时间不远的任务(精细粒度)。
  • t[4][TIME_LEVEL]:远距离时间轮,共 4 层,每层 64 个槽。若一个任务距离现在很远,就被放到更高层的某个槽里。
  • time:定时器的“当前时间”索引,每递增1,就表示经过1个“时间刻度”。
  • spinlock lock:自旋锁,用于并发安全。因为添加定时任务(add_node)可以在别的线程调用,而执行定时器(Timer线程)也会操作同一个结构。

3. 定时器的多级时间轮工作原理

多级时间轮(Multilevel Timer Wheel)可以简单理解为“多个环形数组”,每个数组的槽代表一段时间区间。

  • 最底层(near)精度最高(256个槽,对应最短时间范围)
  • 更高层( t[0..3] )表示越来越大的时间跨度(每层64个槽, 4层)

time每+1,就相当于“最底层时间轮”转动一步 => 过期的任务就会被执行。
若某任务过期时间很远,放到更高层中 => 随着time增加到一定阈值时再“移位”到更低层,直到进入near后就会很快被执行。

3.1 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);
    }
}
  1. 比较过期时间 node->expire 与当前 T->time,判断它们在位运算后是否处于同一“近距离”范围 (time|TIME_NEAR_MASK)

    • 若在同一低位区间 => 放到 near[索引]
    • 否则需要放到更高层( t[i] )
  2. for (i=0; i<3; i++)位运算判断该任务适合放到第几层

    • mask 初始值 = TIME_NEAR<(=256<<6=16384)
    • 通过 (time | (mask-1)) == (current_time | (mask-1)) 来判断任务和当前时间是不是落在同一个对齐区间
    • break 时的 i => 说明就是在 t[i] 这层
  3. 最终 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) 用于判断 timecurrent_time 在低8位里是否相同(即它们只在 0~255 范围内相差不大)。

  • 对更高层时:

    • 这里 (mask-1) 例如 16384-1=16383 (二进制 111111111111111) => 检查更高位范围

    • i=0 => 检查 16k 范围
    • i=1 => 1,048,576 范围
    • i=2 => ...
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);
    }
}
  1. 计算idx = (T->time & TIME_NEAR_MASK) => 定位 near 的哪个槽
  2. link_clear 把这个槽的链表全部取出(要执行的定时节点),然后dispatch_list 分发执行
  3. 如果这槽里还有节点 -> 一直执行

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 还需要把更高层的任务逐层“移位”下来。

  • time++ 到某个临界点,就会把 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;
		}
	}
}

3.3 timer 的时间精度 -- 10ms

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;
}

3.4. 能支持的时间范围

  • near (低8位) => 0 ~ 255 范围
    • T->time % 256 == x, 表示 near[x] 这个槽是当前刻度
  • t[0]: 再往后 64 槽, each 槽表示 256 时间单位 => total可表示 ~256*64=16384 范围
  • t[1]: 64 槽, each 槽 = 16384 => total ~ 16384*64 => 1,048,576
  • ... 以此类推

也就是说 多级 => 可以涵盖相当大的时间跨度(2^8 * 2^(6*4)) * 10ms。

4. 总结

Skynet Timer 通过多级时间轮加上位运算的巧妙组合,使得定时任务的插入与执行都非常高效。近距离(小时间差)和远距离(大时间差)任务分层管理,既照顾了频繁触发的短周期任务,又能处理周期较大的远期任务,并且通过简单的链表操作实现 O(1) 的插入/移除。如果对性能要求不高还可以使用最小堆等,不过时间复杂度 就是 logN 了。

你可能感兴趣的:(skynet,源码阅读,c语言,skynet,timer,时间轮算法)