skynet源码阅读<4>--定时器实现

    昨天和三石公聊天,他提到timer的实现原理,我当时迟疑了一下,心想timer不是系统底层时钟中断驱动上层进程/线程,累积计时实现的么?他简述了timer的实现,什么堆排序,优先级队列等,与我想象的不同。正好这两天在作skynet笔记,以前也没有留意过skynet的timer,这次干脆就看看它是怎么实现的。看了之后我明白了,我与三石公所设想的不是同一个问题。他所关心的问题其实是:框架被注册多个定时回调,如何管理并尽可能高效地触发这些回调。这里我们假设框架将定时消息抽象为timer_node,框架自身最小时间片为T,不同的timer_node按照其将要被触发的时间先后排序,那么只需要在每个时间片T到来时,找到在此时刻Tk的所有timer_node加以触发就可以了。这本质是个排序问题,三石公所述的,其实是排序的不同实现方案而已。言归正传,在分析skynet的代码之前,我们先来就其实现做个简单的说明。

    假设讨论的数值范围为0~999,给定一个数N,我们可以按照以下方式组织:

    首先判断N的大小在哪个层级,这里对应的是【个、十、百】共3个级别,每个级别上分别建立10个桶,以存储加入进来的数据。假设N=2,那么它应落在个位级别上的Bucket2里面;假设N=32,那么它应落在百位级别上的Bucket3里面;假设N=932,那么它应落在百位级别上的Bucket9里面。可以看到,级别越高,如果所划分的桶数不变的话,单个桶中所能容纳的元素就越多,那么从此桶查找目标元素就越耗时。

    设时刻t从0开始,一个时间片为10MS。建立单独一个个位级别的集合S,把个位级别的桶都加进来。t变化时,从S中取出桶来,触发桶中的timer_node。t到10时,S中的元素使用完毕,我们从十位级别拿出桶B0来,把B0中的元素与t作比较。由于t当前已经是十位级别,B0中的元素相对于t此时已经变成个位级别,因此B0内的元素会重新添加到集合S中来。每次当t走完一个新的周期0~9,就重新这个筛选的过程,B1,B2……依次类推。而当t刚增长到百位级别时,它要从百位级别拿出桶B0,其中的元素一部分相对于t是个位级别,一部分相对于t是十位级别,于是前者被添加到S,而后者被重新筛选添加到十位级别的桶中去。在这之后,t每走完一个十位级别的周期(0~10),就要重复十位级别的重新筛选过程;当t走完一个百位级别的周期时(比如从100到200),则要取出下一个百位级别的桶,然后重复百位级别的筛选过程,依次类推。

    在t不断变化的过程中,如果有新的timer_node加入进来,则计算出相对于t的级别,加入到对应的桶中去。

    说完思路,来看看skynet_timer的实现。先看下数据结构的设计:

 1 struct timer_node {
 2     struct timer_node *next;
 3     uint32_t expire;
 4 };
 5 
 6 struct link_list {
 7     struct timer_node head;
 8     struct timer_node *tail;
 9 };
10 
11 struct timer {
12     struct link_list near[TIME_NEAR];
13     struct link_list t[4][TIME_LEVEL];
14     struct spinlock lock;
15     uint32_t time;
16     uint32_t starttime;
17     uint64_t current;
18     uint64_t current_point;
19 };
20 
21 static struct timer * TI = NULL;

    这里TI->near就是我们说的集合S,它分为TIME_NEAR(256)个桶,而TI->t则是其所建立的不同级别的集合,一共4个级别,每个级别分TIME_LEVEL(32)个桶。所以算起来一共有5个级别,第一个级别是0~7位共8位,后面每个级别是6位,所以总共是8+6*4=32位,正好把unit_32用完。TI->time就是我们说的当前时刻t了,它每次以10MS为单位增长。

    看下timer_node是如何加进来的:

 1 static void
 2 add_node(struct timer *T, struct timer_node *node) {
 3     uint32_t time=node->expire;
 4     uint32_t current_time=T->time;
 5     
 6     if ((time|TIME_NEAR_MASK)==(current_time|TIME_NEAR_MASK)) {
 7         link(&T->near[time&TIME_NEAR_MASK],node);
 8     } else {
 9         int i;
10         uint32_t mask=TIME_NEAR << TIME_LEVEL_SHIFT;
11         for (i=0;i<3;i++) {
12             if ((time|(mask-1))==(current_time|(mask-1))) {
13                 break;
14             }
15             mask <<= TIME_LEVEL_SHIFT;
16         }
17 
18         link(&T->t[i][((time>>(TIME_NEAR_SHIFT + i*TIME_LEVEL_SHIFT)) & TIME_LEVEL_MASK)],node);    
19     }
20 }

    在add_node函数中,目标时刻为time(当前时间+duration),current_time为当前时刻。TIME_NEAR_MASK为255,表示最后8位,也就是级别0集合(这里假设5个级别是从0数起),(time|TIME_NEAR_MASK)==(current_time|TIME_NEAR_MASK)的意思是将current_time、time的最后8位都置1,即认为它们只在级别0上不等(全置1后级别0就完全相等了),比较它们在高级别上是否相等。如果是,则其相对级别为0,加入T->near集合,否则判断它们在哪个级别上不等:每次将mask左移TIME_LEVEL_SHIFT(6)位,再进行先|再==的比较,以判断是否在级别1、2、3……上不等。假设mask在第L级别移位后,做time与current_time的先|再==操作,成立的话说明time与current_time仅仅在级别L上是不同的,否则一直向上找,直到找到最高的不同级别LI。以【个、十、百】来比较的话,假设time为5320,current_time为5120,那么会在百位级别上发现不等(先|再==的结果为相等),即相对级别为百位级别,会将元素扔到百位级别的桶中去。再假设time为5320,current_time为20,那么做先|再==的操作,直到千位级别才能结束,即二者相对级别为千位级别,则要将元素扔到千位的桶中去。即找到最大的相对级别后,则要计算出time在这个级别属于哪个桶。首先是(time>>(TIME_NEAR_SHIFT+i*TIME_LEVEL_SHIFT))将此级别段的所有bit移到最右侧,然后&TIME_LEVEL_MASK求余,得到桶号,最后加入进去。

    说完加入,下一步就要看如何执行了:

 1 static void
 2 move_list(struct timer *T, int level, int idx) {
 3     struct timer_node *current = link_clear(&T->t[level][idx]);
 4     while (current) {
 5         struct timer_node *temp=current->next;
 6         add_node(T,current);
 7         current=temp;
 8     }
 9 }
10 
11 static void
12 timer_shift(struct timer *T) {
13     int mask = TIME_NEAR;
14     uint32_t ct = ++T->time;
15     if (ct == 0) {
16         move_list(T, 3, 0);
17     } else {
18         uint32_t time = ct >> TIME_NEAR_SHIFT;
19         int i=0;
20 
21         while ((ct & (mask-1))==0) {
22             int idx=time & TIME_LEVEL_MASK;
23             if (idx!=0) {
24                 move_list(T, i, idx);
25                 break;                
26             }
27             mask <<= TIME_LEVEL_SHIFT;
28             time >>= TIME_LEVEL_SHIFT;
29             ++i;
30         }
31     }
32 }
33 
34 static inline void
35 timer_execute(struct timer *T) {
36     int idx = T->time & TIME_NEAR_MASK;
37     
38     while (T->near[idx].head.next) {
39         struct timer_node *current = link_clear(&T->near[idx]);
40         SPIN_UNLOCK(T);
41         // dispatch_list don't need lock T
42         dispatch_list(current);
43         SPIN_LOCK(T);
44     }
45 }
46 
47 static void 
48 timer_update(struct timer *T) {
49     SPIN_LOCK(T);
50 
51     // try to dispatch timeout 0 (rare condition)
52     timer_execute(T);
53 
54     // shift time first, and then dispatch timer message
55     timer_shift(T);
56 
57     timer_execute(T);
58 
59     SPIN_UNLOCK(T);
60 }

    timer_update中先execute,根据当前时刻TI->time取出TI->near中timer_node并向目标分发消息。然后做关键的timer_shift,当前时刻TI->time加1,此时就要判断它是否处于不同级别的周期临界上,从上面的说明我们知道,当它在某个级别Ln时,它需要将Ln中下一个桶中的元素取出重新筛选到L0~Ln-1各级别中去==>

    (mask-1)的初始值是(TIME_NEAR-1)(255,级别0范围),而ct(++TI->time,当前时间)与(mask-1)做&操作,也就是在级别0范围内求余,如果不为0的话,说明ct是在级别0内增加的,比如从2-->3。反之则说明当前时间ct在大于0的级别。在此情况下,time初值已经是ct>>TIME_NEAR_SHIFT了,其与TIME_LEVEL_MASK做&操作,即在级别1范围内求余。如果为0的话说明在大于1的级别,跳出;否则mask继续左移TIME_LEVEL_SHIFT以扩大级别范围,time则继续右移TIME_LEVEL_SHIFT在新级别内求余。当余数idx不为0时,表明ct在这个级别内增加了(比如从299->300,以【个、十、百】比较的话),那么此时就要取出这个level级别的桶idx内的元素重新筛选,根据相对于ct的级别重新分配到低级别的桶中去。move_list所做的,便是这个重新分配的过程。

    至此,算法的详细过程已经分析完毕了。其思想是只关注较近时间段内的timer_node排序,限制了每次处理的最小集合。而实际使用定时器,一般是时间小的定时器居多,时间越大,这种定时器实际使用的情况越少,因此在高级别桶内的元素数目不会很多,重新筛选分配的开销也不大。最后,再看看在skynet_start.c中,是如何驱动skynet_timer的:

 1 static void *
 2 thread_timer(void *p) {
 3     struct monitor * m = p;
 4     skynet_initthread(THREAD_TIMER);
 5     for (;;) {
 6         skynet_updatetime();
 7         CHECK_ABORT
 8         wakeup(m,m->count-1);
 9         usleep(2500);
10     }
11 // other
...
}

    可以看到,是开启了一个单独的线程,每隔2500微秒(2.5毫秒)来驱动的。

转载于:https://www.cnblogs.com/Jackie-Snow/p/6559381.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值