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

本文探讨了时间轮定时器在游戏开发场景下的优势及应用,通过实例展示了如何利用时间轮定时器实现建筑建造时间管理,提高了游戏逻辑处理的效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

高性能定时器
定时器的结构有多钟比如链表式,最小堆,时间轮的 在不同应用场景下使用哪种需要考虑效率和复杂度
这次我么那先先讲讲时间轮定时器,在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);


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值