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 };
View Code
复制代码

  时间轮的具体设计如下:

  所有的时间事件分为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 }
View Code
复制代码

  当处理完最近的事件时,每次都需要判断当前time是否已经执行了一个轮(即255个事件tick),如果是,则需要根据当前time计算出已经执行到了哪一等级的时间中,并且重新分配当前的事件等级里的事件。

  虽然后面等级的事件队列可能会有很多事件,但是对于游戏框架而言,如果一个timeout事件超过了一天或者7天时,则可能是设计问题,可能更改逻辑会比较好。

  对于游戏逻辑而言,时间任务可能在几秒内的逻辑会比较多,所以执行并且重新分配的过程并不会很多。

  时间轮的插入算法,时间复杂度不随着事件的数量变化而变化,是o(1),获取的时间复杂度,只需要获取当前队列即可,所以也是o(1),执行完当前的队列后,需要重新分配下一等级的队列,这个操作的数据是比较少的,是每2.55秒执行一次,所以消耗还是比较少的。

 

posted @   小乐虎  阅读(266)  评论(0编辑  收藏  举报
编辑推荐:
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示