skynet源码 --- skynet_timer.c
一个网络框架,需要能够处理 网络消息,系统信号,定时任务 等事件,接下来我们一起研究skynet的定时任务是如何实现的。
传统的定时任务,比较高效的是时间轮算法,时间堆算法(最小堆),skynet采用了时间轮的算法去处理。
skynet_timer.c在框架中主要作用在于管理skyent自身的时间戳,以及管理由skynet.timeout()创建出来的时间事件。
在skynet中,会创建一个timer线程来更新skynet时间,运行周期为2500微秒,每次执行都会去读取系统时间,并且更新skynet的时间结构信息。
定时任务的结构体如下,skynet使用tick作为一次帧操作,即time字段,这个time只会增加不会减少(除了超过32位时重置为0),代表进程启动到现在所经历过的tick数,一个tick为10ms。

1 struct timer_event { // 定时任务的事件结构 2 uint32_t handle; 3 int session; 4 }; 5 6 struct timer_node { // 时间轮里的具体任务信息 7 struct timer_node *next; 8 uint32_t expire; 9 }; 10 11 struct link_list { // 时间轮列表信息 12 struct timer_node head; 13 struct timer_node *tail; 14 }; 15 16 struct timer { 17 struct link_list near[TIME_NEAR]; // 当前轮最近64个tick会发生的定时器(当前轮为0开始计算),即当前tick为62时,near中63存的时下一次tick要执行的定时器 18 struct link_list t[4][TIME_LEVEL]; // 非最近要执行的定时器,idx越大表示离当前时间轮越远 19 struct spinlock lock; // 自旋锁 20 uint32_t time; // 当前已经运行过ticktock的次数 21 uint32_t starttime; // skynet启动的时间戳(秒) 22 uint64_t current; // 当前的系统时间戳(秒,小数点2位) 23 uint64_t current_point; // 系统启动到现在的时间段值 24 };
时间轮的具体设计如下:
所有的时间事件分为5个等级,全部有tick数(time字段)来计算具体放在哪个时间链表下,
第一个级别为即将要执行的事件,由int32位的前8位与当前tick跟目标tick相与来判断目标tick是否在当前执行轮中的8位(即当前轮的255个帧中,2.55s)中执行到,
第二级为即将要发生但是目标tick不在当前轮,但是在14位中执行到的值,以此类推,
第三级为20位,第四级为26位,第五级位32位,值得注意的是第五级存储的时间事件是大于26位的所有时间事件,并不只是32位内的事件,第五级的下标为0的事件链表里保存的是当目标tick为0的时候,即刚好是32位+1的时候的事件,所以要特殊处理。
当一个时间事件加入到队列中,会等待主流程经过一个tick时去根据当前的time值去分配队列里的时间:

1 static void 2 timer_shift(struct timer *T) { 3 int mask = TIME_NEAR; 4 uint32_t ct = ++T->time; 5 if (ct == 0) { // 当执行了 2^32 次方时,当前tick重新变为0时,重新分配最后一档的定时任务,3,0 保存的数据是下一轮(2的32次方)的定时任务,当 32个位都为0时才会保存到这里 6 move_list(T, 3, 0); 7 } else { 8 uint32_t time = ct >> TIME_NEAR_SHIFT; // 将当前tick右移8位,用来判断是否需要将下一档的定时任务重新分配 9 int i=0; 10 11 while ((ct & (mask-1))==0) { // 如果等于0,则表示刚好执行完了下一档的数量,下一档需要重新分配(0,1,2 中的档位里idx=0这个链表里没有数据) 12 int idx=time & TIME_LEVEL_MASK; 13 if (idx!=0) { 14 move_list(T, i, idx); 15 break; 16 } 17 mask <<= TIME_LEVEL_SHIFT; 18 time >>= TIME_LEVEL_SHIFT; 19 ++i; 20 } 21 } 22 } 23 24 static void 25 timer_update(struct timer *T) { 26 SPIN_LOCK(T); 27 28 // try to dispatch timeout 0 (rare condition) 29 timer_execute(T); 30 31 // shift time first, and then dispatch timer message 32 timer_shift(T); 33 34 timer_execute(T); 35 36 SPIN_UNLOCK(T); 37 }
当处理完最近的事件时,每次都需要判断当前time是否已经执行了一个轮(即255个事件tick),如果是,则需要根据当前time计算出已经执行到了哪一等级的时间中,并且重新分配当前的事件等级里的事件。
虽然后面等级的事件队列可能会有很多事件,但是对于游戏框架而言,如果一个timeout事件超过了一天或者7天时,则可能是设计问题,可能更改逻辑会比较好。
对于游戏逻辑而言,时间任务可能在几秒内的逻辑会比较多,所以执行并且重新分配的过程并不会很多。
时间轮的插入算法,时间复杂度不随着事件的数量变化而变化,是o(1),获取的时间复杂度,只需要获取当前队列即可,所以也是o(1),执行完当前的队列后,需要重新分配下一等级的队列,这个操作的数据是比较少的,是每2.55秒执行一次,所以消耗还是比较少的。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)