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

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


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


如图:

游戏后台之高效定时器-时间轮_第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);


你可能感兴趣的:(网游服务器开发,开源&工具)