高性能定时器

1 时间轮

       轮中的实线指针指向轮子上的一个槽(slot),它以恒定的速度顺时针转动,每转动一步就指向下一个槽,每次转动称为一个滴答(tick)。一个滴答的时间称为是间轮的槽间隔si(slot interval),它实际上就是心跳时间。该轮共有N个槽,因此它运转一周的时间是N×si 。每个槽指向一条定时器链表,每条链表上的定时器具有相同的特性:它们的定时时间相差N×si的整数倍。时间轮正是利用这个关系将定时器散列到不同的链表中。

        假如现在指针指向槽cs,我们要添加一个定时时间为ti的定时器,则该定时器将被插入ts(timer slot)对应的链表中:

                                                                     ts = (cs + (ti / si)) %N

        基于排序链表的定时器使用唯一的一条链表来管理所有定时器,所以插入操作的效率随着定时器数目的增多而降低。而时间轮使用哈希表的思想,将定时器散列到不同的链表上。对时间轮而言,添加一个定时器的时间复杂度是O(1), 删除一个定时器的时间复杂度是O(1),执行一个定时器的时间复杂度是O(n)。

       可以看出,要想提高时间轮的定时精度,就要使si值足够小;要提高执行效率,则要求N值足够大。

       上图描述的是一种简单的时间轮,因为它只有一个轮子,而复杂的时间轮可能由不同时间维度的轮子组成,相邻的两个轮子,精度高的转动一圈,精度低的只走动一槽,就和水表一样。下面来编写一个简单的时间轮算法。

 #ifndef TIME_WHEEL_TIMER
 #define TIME_WHEEL_TIMER
 
 #include <time.h>
 #include <netinet/in.h>
 #include <stdio.h>
 
 #define BUFFER_SIZE 64
 class tw_timer;
 struct client_data       //绑定socket和定时器
 {
     sockaddr_in address;
     int sockfd;
     char buf[ BUFFER_SIZE ];
     tw_timer* timer;
 };
 
 class tw_timer           //定时器类
 {
 public:
     tw_timer( int rot, int ts ) 
     : next( NULL ), prev( NULL ), rotation( rot ), time_slot( ts ){}
 
 public:
     int rotation;         //记录该定时器在时间轮转多少圈后生效
     int time_slot;         //记录定时器属于那个槽
     void (*cb_func)( client_data* );//定时器回调函数
     client_data* user_data;         //用户数据
     tw_timer* next;                 //指向上一个定时器
     tw_timer* prev;                //指向下一个定时器
 };
 
 class time_wheel//事件轮管理定时器
 {
 public:
     time_wheel() : cur_slot( 0 )
     {
         for( int i = 0; i < N; ++i )
         {
             slots[i] = NULL;            //每个槽的头节点初始化为空
         }
     }
     ~time_wheel()
     {
         for( int i = 0; i < N; ++i )
         {
             tw_timer* tmp = slots[i];
             while( tmp )
             {
                 slots[i] = tmp->next;
                 delete tmp;            //遍历每个槽销毁new分配在堆中的定时器
                 tmp = slots[i];
             }
         }
     }
     tw_timer* add_timer( int timeout )//添加新的定时器,插入到合适的槽中
     {
         if( timeout < 0 )//时间错误
         {
             return NULL;
         }
         int ticks = 0;
         if( timeout < TI )//小于每个槽的interval,则为1
         {
             ticks = 1;
         }
         else
         {
             ticks = timeout / TI;            //相对当前位置的槽数
         }
         int rotation = ticks / N;             //记录多少圈后生效
         int ts = ( cur_slot + ( ticks % N ) ) % N;//确定插入槽的位置
         tw_timer* timer = new tw_timer( rotation, ts );//根据位置和圈数,插入对应的槽中
         if( !slots[ts] )                   //所在槽头节点为空,直接插入
         {
             printf( "add timer, rotation is %d, ts is %d, cur_slot is %d\n", rotation, ts, cur_slot );
             slots[ts] = timer;
         }
         else            //头插法
         {
             timer->next = slots[ts];
             slots[ts]->prev = timer;
             slots[ts] = timer;
         }
         return timer;//返回含有时间信息和所在槽位置的定时器
     }
     void del_timer( tw_timer* timer )//从时间轮上删除定时器
     {
         if( !timer )
         {
             return;
         }
         int ts = timer->time_slot;//找到所在槽
         if( timer == slots[ts] )
         {
             slots[ts] = slots[ts]->next;
             if( slots[ts] )
             {
                 slots[ts]->prev = NULL;
             }
             delete timer;
         }
         else
         {
             timer->prev->next = timer->next;
             if( timer->next )
             {
                 timer->next->prev = timer->prev;
             }
             delete timer;
         }
     }
     void tick()
     {
         tw_timer* tmp = slots[cur_slot];//取出当前槽的头节点
         printf( "current slot is %d\n", cur_slot );
         while( tmp )//遍历
         {
             printf( "tick the timer once\n" );
             if( tmp->rotation > 0 )
             {
                 tmp->rotation--;
                 tmp = tmp->next;
             }
             else
             {
                 tmp->cb_func( tmp->user_data );//符合条件,调用回调函数
                 if( tmp == slots[cur_slot] )
                 {
                     printf( "delete header in cur_slot\n" );
                     slots[cur_slot] = tmp->next;
                     delete tmp;
                     if( slots[cur_slot] )
                     {
                         slots[cur_slot]->prev = NULL;
                     }
                     tmp = slots[cur_slot];
                 }
                 else
                 {
                     tmp->prev->next = tmp->next;
                     if( tmp->next )
                     {
                         tmp->next->prev = tmp->prev;
                     }
                     tw_timer* tmp2 = tmp->next;
                     delete tmp;
                     tmp = tmp2;
                 }
             }
         }
         cur_slot = ++cur_slot % N;
     }
 
 private:
      
     static const int N = 60;    // 时间轮上槽的数目
     static const int TI = 1;    //每1s 时间轮转动一次, 即槽时间间隔为1 s
     tw_timer* slots[N];         //时间轮的槽,其中每一个元素指向一个定时器链表,链表无序
     int cur_slot;               //当前槽位
 };
 
 #endif

       单级时间轮处理大时间的效率不太令人满意,因此需要将时间事件按照时间层级放置,层级时间轮是将不同的时间放在对应的时间轮中,当一个时间小于当前时间轮的最小刻度,就会被下放到低一级的时间轮中。

       由上图可知,第一层时间轮的interval是20ms,第二层时间轮的interval是400ms,第三层时间轮的interval是8000ms。当来一个350ms的定时任务,因第一层不满足要求会被放在第二层的时间轮中,但过了330ms后因任务时间不足20ms,该任务会被降级到第一层的时间轮中。等到了任务到期,最终执行相应的到期操作。

参考Kafka解惑之时间轮(TimingWheel)

2 时间堆

       设计定时器的另外一种思路是:将所有定时器中超时时间最小的一个作为心跳间隔。这样,一旦心跳函数tick被调用,超时时间最小的定时器必然到期,我们就可以在tick函数中处理该定时器。然后再次从剩余的定时器中找出超时时间最小的一个,并将这段最小时间设置为下一次心跳间隔。如此反复,就实现了较为精确的定时。

参考linux网络编程二十三:高性能定时器之时间堆


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值