高效定时器:时间轮和时间堆

                                                                            游戏后台之高效定时器-时间轮

原文地址:http://blog.csdn.net/soft2967/article/details/9274691

高性能定时器
定时器的结构有多种,比如链表式,最小堆,时间轮的 ,在不同应用场景下使用哪种需要考虑效率和复杂度
这次我么那先先讲讲时间轮定时器,在linux内核里这种结构的定时器大量使用。
1.升序链表定时器
   
时间轮定时器
1.时间轮定时器有什么好处,或者说这种结构的定时器能决解什么问题?
在上面的升序链表定时器里,可以看到在添加一个定时器的时候,复杂度是O(n)
因为要保持有序性,所以的遍历链表插入到合适的位置。假设系统有大量的定时器(10W个)
使用升序链表型的就会有性能问题。这时时间轮定时器就会比较适合。


常用定时器实现算法复杂度 
实现方式 StartTimerStopTimerPerTickBookkeeping
基于链表 O(1)     O(n)    O(n)
基于排序链表 O(n)     O(1)    O(1)
基于最小堆 O(lgn)     O(1)    O(1)
基于时间轮 O(1)     O(1)    O(1)


如图:


假设有N个槽,时间轮已恒定速度顺时针转动,每转动一步槽指针就指向下一个槽,每转动一次的时间间隔叫做一个滴答间隔si,
这样转动一周的时间为 T = si*N ,每个槽都是一个链表。这样在插入定时器的时候可以直接计算出要放在那个槽。

假设在T时间后到期,insertslot = (curslot + (T/si)) % N,计算出了insertslot就可以在O(1)的复杂度里完成。

//下面是简单的时间轮定时器代码

class tw_timer;


struct client_data
{
    unsigned int uUin; //角色ID
    unsigned int utype; //建筑类型
    tw_timer* timer;
};


typedef void (*pFUNC)(client_data*);
class tw_timer
{
public:
    //rot轮转几圈定时器到期
    //ts 槽的索引
    tw_timer( int rot, int ts ,pFUNC TimeOutCall) 
    : next( NULL ), prev( NULL ), rotation( rot ), time_slot( ts )
    {
        TimeOutfunc = TimeOutCall;
    }


public:
    //轮转几圈定时器到期
    int rotation;
    // 槽的索引
    int time_slot;
    //到时后的回调函数
    //void (*cb_func)( client_data* );
    pFUNC TimeOutfunc;
    //自定义函数
    client_data* user_data;
    //链表的指针
    tw_timer* next;
    tw_timer* prev;
};


class time_wheel
{
public:
    time_wheel() : cur_slot( 0 ))
    {
        //获得服务器时间
        LastTickTime = GetCurTime(); 
        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;
                tmp = slots[i];
            }
        }
    }
    tw_timer* add_timer( int timeout, pFUNC TimeOutCall)
    {
        if( timeout < 0 )
        {
            return NULL;
        }
        int ticks = 0;
        //最少要一个滴答间隔
        if( timeout < TI )
        {
            ticks = 1;
        }
        else
        {
            ticks = timeout / TI;
        }
        //rotation为0表示定时器到期
        int rotation = ticks / N;
        //计算槽索引
        int ts = ( cur_slot + ( ticks % N ) ) % N;
        tw_timer* timer = new tw_timer( rotation, ts ,TimeOutCall);
        //当前的槽上没有定时器就放在head位置,否则放在插入在head位置
        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;
        }
    }


    //每一个滴答间隔调用一次tick函数 time为当前服务器时间
    void tick(unsigned int time)
    {
        //计算更新间隔经过了多少个滴答
        unsigned int Ticount = (time - LastTickTime)/TI; 
        tw_timer* tmp = slots[cur_slot];
        printf( "current slot is %d\n", cur_slot );
        for(int i = 0;i < Ticount; ++i)
        {
            while( tmp )
            {
                printf( "tick the timer once\n" );
                if( tmp->rotation > 0 )
                {
                    tmp->rotation--;
                    tmp = tmp->next;
                }
                else
                {
                    tmp->TimeOutfunc( 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;
                    }
                }
            }
            //移动到下一个槽,时间轮是环所以需要%N
        cur_slot = ++cur_slot % N;
       }
        LastTickTime = time;
    }


private:
    //槽个数
    static const int N = 60;
    //滴答间隔(每移动一个槽的时间间隔)
    static const int TI = 1; 
    //时间轮
    tw_timer* slots[N];
    //当前槽索引
    int cur_slot;
    //最后更新
    unsigned int LastTickTime;
};


//假设在后台如何使用了

后台都会有一个主循环大概如下

bool update()
{
    while(!stopserver)
    {
        //读网络IO
        //读DB数据包
        //处理事件
        //处理定时器
        timewhel.tick();
        //处理逻辑
    }
  
}

//就在住循环里驱动我们的定时器,在调用tick函数

比如我们现在有这么个个需求,就是玩家可以建造各式各样的建筑,比如房子,兵营,田地等,被建造的建筑会在一定时间后才能完成,并通知给前台,这样就需要一个定时器

//建造人口房屋
void BuilderHouse(client_data* clietdata)
{
    //伪代码逻辑
    /*
    if (NULL == clietdata)
    {
        LOG("XXX");
        return;
    }
    
    CRole* pRole = FindRole(clietdata->uUin);
    if (NULL == pRole)
    {
         LOG("XXX");
         return;
    }
    //调用角色建造人口接口,处理后台逻辑
    pRole->BuilderHouse();

    //通知给前台

    Send(msg);
    */
}


//建造兵营
void BuilderCamp(client_data* clietdata)
{
    //同上
}


//建造田地
void BuilderField(client_data* clietdata)
{
    //同上
}


static time_wheel timewhel;

  //假设玩家在游戏里场景里创建了一个房子,会执行下行代码

int CmdBuild()
{    
    //房子建造完成需要3分钟(180s) ,BuilderHouse为完成后的回调函数
    timewhel.add_timer(180,BuilderHouse);

epoll+时间堆定时器

原文地址:http://blog.csdn.net/w616589292/article/details/46336309

在开发Linux网络程序时,通常需要维护多个定时器,如维护客户端心跳时间、检查多个数据包的超时重传等。如果采用Linux的SIGALARM信号实现,则会带来较大的系统开销,且不便于管理。

本文在应用层实现了一个基于时间堆的高性能定时器,同时考虑到定时的粒度问题,由于通过alarm系统调用设置的SIGALARM信号只能以秒为单位触发,因此需要采用其它手段实现更细粒度的定时操作,当然,这里不考虑使用多线程+sleep的实现方法,理由性能太低。

通常的做法还有采用基于升序的时间链表,但升序时间链表的插入操作效率较低,需要遍历链表。因此本实现方案使用最小堆来维护多个定时器,插入O(logn)、删除O(1)、查找O(1)的效率较高。

首先是每个定时器的定义:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. class heap_timer  
  2. {  
  3. public:  
  4.     heap_timer( int ms_delay )  
  5.     {  
  6.         gettimeofday( &expire, NULL );  
  7.         expire.tv_usec += ms_delay * 1000;  
  8.         if ( expire.tv_usec > 1000000 )  
  9.         {  
  10.             expire.tv_sec += expire.tv_usec / 1000000;  
  11.             expire.tv_usec %= 1000000;  
  12.         }  
  13.     }  
  14.   
  15. public:  
  16.     struct timeval expire;  
  17.     void (*cb_func)( client_data* );  
  18.     client_data* user_data;  
  19.     ~heap_timer()  
  20.     {  
  21.         delete user_data;  
  22.     }  
  23. };  

包括一个超时时间expire、超时回调函数cb_func以及一个user_data变量,user_data用于存储与定时器相关的用户数据,用户数据可以根据不同的应用场合进行修改,这里实现的是一个智能博物馆的网关,网关接收来自zigbee协调器的用户数据,并为每个用户维护一段等待时间T,在T到来之前,同一个用户的所有数据都存放到user_data的target_list中,当T到来时,根据target_list列表选择一个适当的target并发送到ip_address,同时删除定时器(有点扯远了=。=)。总之,要实现的功能就是给每个用户维护一个定时器,定时值到来时做一些操作。

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. class client_data  
  2. {  
  3. public:  
  4.     client_data(char *address):target_count(0)  
  5.     {  
  6.         strcpy(ip_address,address);  
  7.     }  
  8. private:  
  9.     char ip_address[32];  
  10.     target target_list[64];  
  11.     int target_count;  
  12.     ......  
  13. };  
 

以下是时间堆的类定义,包括了一些基本的堆操作:插入、删除、扩容,还包括了定时器溢出时的操作函数tick()

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. class time_heap  
  2. {  
  3. public:  
  4.     time_heap( int cap  = 1) throw ( std::exception )  
  5.         : capacity( cap ), cur_size( 0 )  
  6.     {  
  7.         array = new heap_timer* [capacity];  
  8.         if ( ! array )  
  9.         {  
  10.             throw std::exception();  
  11.         }  
  12.         forint i = 0; i < capacity; ++i )  
  13.         {  
  14.             array[i] = NULL;  
  15.         }  
  16.     }  
  17.   
  18.     ~time_heap()  
  19.     {  
  20.         for ( int i =  0; i < cur_size; ++i )  
  21.         {  
  22.             delete array[i];  
  23.         }  
  24.         delete [] array;  
  25.     }  
  26.   
  27. public:  
  28.     int get_cursize()  
  29.     {  
  30.         return cur_size;  
  31.     }  
  32.   
  33.     void add_timer( heap_timer* timer ) throw ( std::exception )  
  34.     {  
  35.         if( !timer )  
  36.         {  
  37.             return;  
  38.         }  
  39.         if( cur_size >= capacity )  
  40.         {  
  41.             resize();  
  42.         }  
  43.         int hole = cur_size++;  
  44.         int parent = 0;  
  45.         for( ; hole > 0; hole=parent )  
  46.         {  
  47.             parent = (hole-1)/2;  
  48.             if ( timercmp( &(array[parent]->expire), &(timer->expire), <= ) )  
  49.             {  
  50.                 break;  
  51.             }  
  52.             array[hole] = array[parent];  
  53.         }  
  54.         array[hole] = timer;  
  55.     }  
  56.     void del_timer( heap_timer* timer )  
  57.     {  
  58.         if( !timer )  
  59.         {  
  60.             return;  
  61.         }  
  62.         // lazy delelte  
  63.         timer->cb_func = NULL;  
  64.     }  
  65.     int top(struct timeval &time_top) const  
  66.     {  
  67.         if ( empty() )  
  68.         {  
  69.             return 0;  
  70.         }  
  71.         time_top = array[0]->expire;  
  72.         return 1;  
  73.     }  
  74.     void pop_timer()  
  75.     {  
  76.         if( empty() )  
  77.         {  
  78.             return;  
  79.         }  
  80.         if( array[0] )  
  81.         {  
  82.             delete array[0];  
  83.             array[0] = array[--cur_size];  
  84.             percolate_down( 0 );  
  85.         }  
  86.     }  
  87.     void tick()  
  88.     {  
  89.         heap_timer* tmp = array[0];  
  90.         struct timeval cur;  
  91.         gettimeofday( &cur, NULL );  
  92.         while( !empty() )  
  93.         {  
  94.             if( !tmp )  
  95.             {  
  96.                 break;  
  97.             }  
  98.             if( timercmp( &cur, &(tmp->expire), < ) )  
  99.             {  
  100.                 break;  
  101.             }  
  102.             if( array[0]->cb_func )  
  103.             {  
  104.                 array[0]->cb_func( array[0]->user_data );  
  105.             }  
  106.             pop_timer();  
  107.             tmp = array[0];  
  108.         }  
  109.     }  
  110.     bool empty() const  
  111.     {  
  112.         return cur_size == 0;  
  113.     }  
  114.     heap_timer** get_heap_array()  
  115.     {  
  116.         return array;  
  117.     }  
  118.   
  119. private:  
  120.     void percolate_down( int hole )  
  121.     {  
  122.         heap_timer* temp = array[hole];  
  123.         int child = 0;  
  124.         for ( ; ((hole*2+1) <= (cur_size-1)); hole=child )  
  125.         {  
  126.             child = hole*2+1;  
  127.             if ( (child < (cur_size-1)) && timercmp( &(array[child+1]->expire), &(array[child]->expire), < ) )  
  128.             {  
  129.                 ++child;  
  130.             }  
  131.             if ( timercmp( &(array[child]->expire), &(temp->expire), < ) )  
  132.             {  
  133.                 array[hole] = array[child];  
  134.             }  
  135.             else  
  136.             {  
  137.                 break;  
  138.             }  
  139.         }  
  140.         array[hole] = temp;  
  141.     }  
  142.     void resize() throw ( std::exception )  
  143.     {  
  144.         heap_timer** temp = new heap_timer* [2*capacity];  
  145.         forint i = 0; i < 2*capacity; ++i )  
  146.         {  
  147.             temp[i] = NULL;  
  148.         }  
  149.         if ( ! temp )  
  150.         {  
  151.             throw std::exception();  
  152.         }  
  153.         capacity = 2*capacity;  
  154.         for ( int i = 0; i < cur_size; ++i )  
  155.         {  
  156.             temp[i] = array[i];  
  157.         }  
  158.         delete [] array;  
  159.         array = temp;  
  160.     }  
  161.   
  162.   
  163. private:  
  164.     heap_timer** array;  
  165.     int capacity;  
  166.     int cur_size;  
  167. };  

如何用epoll实现多个定时器的操作是本设计的关键,我们知道,epoll_wait的最后一个参数是阻塞等待的时候,单位是毫秒。可以这样设计:

1、当时间堆中没有定时器时,epoll_wait的超时时间T设为-1,表示一直阻塞等待新用户的到来;

2、当时间堆中有定时器时,epoll_wait的超时时间T设为最小堆堆顶的超时值,这样可以保证让最近触发的定时器能得以执行;

3、在epoll_wait阻塞等待期间,若有其它的用户到来,则epoll_wait返回n>0,进行常规的处理,随后应重新设置epoll_wait为小顶堆堆顶的超时时间。

为此,本实现对epoll_wait进行了封装,名为tepoll_wait,调用接口与epoll_wait差不多,但返回值有所不同:tepoll_wait不返回n=0的情况(即超时),因为超时事件在tepoll_wait中进行处理,只有等到n>0(即在等待过程中有用户数据到来)或者n<0(出现错误)才进行返回。

废话不多说,看代码最清楚:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. void timer_handler()  
  2. {  
  3.     heap.tick();  
  4.     //setalarm();  
  5. }  
  6.   
  7. /* tselect - select with timers */  
  8. int tepoll_wait( int epollfd, epoll_event *events, int max_event_number )  
  9. {  
  10.     struct timeval now;  
  11.     struct timeval tv;  
  12.     struct timeval *tvp;  
  13.     //tevent_t *tp;  
  14.     int n;  
  15.   
  16.     for ( ;; )  
  17.     {  
  18.         if ( gettimeofday( &now, NULL ) < 0 )  
  19.             perror("gettimeofday");  
  20.         struct timeval time_top;  
  21.         if ( heap.top(time_top) )  
  22.         {  
  23.             tv.tv_sec = time_top.tv_sec - now.tv_sec;;  
  24.             tv.tv_usec = time_top.tv_usec - now.tv_usec;  
  25.             if ( tv.tv_usec < 0 )  
  26.             {  
  27.                 tv.tv_usec += 1000000;  
  28.                 tv.tv_sec--;  
  29.             }  
  30.             tvp = &tv;  
  31.         }  
  32.         else  
  33.             tvp = NULL;  
  34.   
  35.         if(tvp == NULL)  
  36.             n = epoll_wait( epollfd, events, max_event_number, -1 );  
  37.         else  
  38.             n = epoll_wait( epollfd, events, max_event_number, tvp->tv_sec*1000 + tvp->tv_usec/1000 );  
  39.         if ( n < 0 )  
  40.             return -1;  
  41.         if ( n > 0 )  
  42.             return n;  
  43.   
  44.         timer_handler();  
  45.     }  
  46. }  
代码一目了然,在tepoll_wait中,是个死循环,只有等到上述两种情况发生时,才进行返回,此时在调用方进行处理,处理过程跟epoll_wait一样。

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. while( !stop_server )  
  2.     {  
  3.         number = tepoll_wait( epollfd, events, MAX_EVENT_NUMBER);  
  4.         for ( i= 0; i < number; i++ )  
  5.         {  
  6.             int fd = events[i].data.fd;  
  7.             if ( (events[i].events & EPOLLIN)&& (fd == uart_fd) )  
  8.             {  
  9.                //读取用户数据  
  10.                 if( (timer_id = find_exist_timer(ip_address)) != -1)  
  11.                 {  
  12.                     //add to the exist timer  
  13.                     heap_timer ** heap_array = heap.get_heap_array();  
  14.                     heap_array[timer_id]->user_data->add_target(RSSI,target_id);  
  15.                     continue;  
  16.                 }  
  17. <span style="white-space:pre">      </span>//new timer  
  18.                 heap_timer *timer = new heap_timer(200);  
  19.                 timer->cb_func = cb_func;  
  20.                 timer->user_data = new client_data(ip_address);  
  21.                 timer->user_data->add_target(RSSI,target_id);  
  22.                 heap.add_timer(timer);  
  23.             }  
  24.             else if( ( fd == pipefd[0] ) && ( events[i].events & EPOLLIN ) )  
  25.             {  
  26.                 //此处进行了统一信号源处理,通过双向管道来获取SIGTERM以及SIGINT的信号,在主循环中进行统一处理  
  27. <span style="white-space:pre">      </span>char signals[1024];  
  28.                 ret = recv( pipefd[0], signals, sizeof( signals ), 0 );  
  29.                 if( ret == -1 )  
  30.                 {  
  31.                     continue;  
  32.                 }  
  33.                 else if( ret == 0 )  
  34.                 {  
  35.                     continue;  
  36.                 }  
  37.                 else  
  38.                 {  
  39.                     forint i = 0; i < ret; ++i )  
  40.                     {  
  41.                         switch( signals[i] )  
  42.                         {  
  43.                         case SIGTERM:  
  44.                         case SIGINT:  
  45.                         {  
  46.                             stop_server = true;  
  47.                         }  
  48.   
  49.                         }  
  50.                     }  
  51.                 }  
  52.             }  
  53.         }  
  54.     }  


  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值