游戏后台之高效定时器-时间轮
原文地址: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)
如图:
这样转动一周的时间为 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+时间堆定时器
在开发Linux网络程序时,通常需要维护多个定时器,如维护客户端心跳时间、检查多个数据包的超时重传等。如果采用Linux的SIGALARM信号实现,则会带来较大的系统开销,且不便于管理。
本文在应用层实现了一个基于时间堆的高性能定时器,同时考虑到定时的粒度问题,由于通过alarm系统调用设置的SIGALARM信号只能以秒为单位触发,因此需要采用其它手段实现更细粒度的定时操作,当然,这里不考虑使用多线程+sleep的实现方法,理由性能太低。
通常的做法还有采用基于升序的时间链表,但升序时间链表的插入操作效率较低,需要遍历链表。因此本实现方案使用最小堆来维护多个定时器,插入O(logn)、删除O(1)、查找O(1)的效率较高。
首先是每个定时器的定义:
- class heap_timer
- {
- public:
- heap_timer( int ms_delay )
- {
- gettimeofday( &expire, NULL );
- expire.tv_usec += ms_delay * 1000;
- if ( expire.tv_usec > 1000000 )
- {
- expire.tv_sec += expire.tv_usec / 1000000;
- expire.tv_usec %= 1000000;
- }
- }
- public:
- struct timeval expire;
- void (*cb_func)( client_data* );
- client_data* user_data;
- ~heap_timer()
- {
- delete user_data;
- }
- };
包括一个超时时间expire、超时回调函数cb_func以及一个user_data变量,user_data用于存储与定时器相关的用户数据,用户数据可以根据不同的应用场合进行修改,这里实现的是一个智能博物馆的网关,网关接收来自zigbee协调器的用户数据,并为每个用户维护一段等待时间T,在T到来之前,同一个用户的所有数据都存放到user_data的target_list中,当T到来时,根据target_list列表选择一个适当的target并发送到ip_address,同时删除定时器(有点扯远了=。=)。总之,要实现的功能就是给每个用户维护一个定时器,定时值到来时做一些操作。
- class client_data
- {
- public:
- client_data(char *address):target_count(0)
- {
- strcpy(ip_address,address);
- }
- private:
- char ip_address[32];
- target target_list[64];
- int target_count;
- ......
- };
以下是时间堆的类定义,包括了一些基本的堆操作:插入、删除、扩容,还包括了定时器溢出时的操作函数tick()
- class time_heap
- {
- public:
- time_heap( int cap = 1) throw ( std::exception )
- : capacity( cap ), cur_size( 0 )
- {
- array = new heap_timer* [capacity];
- if ( ! array )
- {
- throw std::exception();
- }
- for( int i = 0; i < capacity; ++i )
- {
- array[i] = NULL;
- }
- }
- ~time_heap()
- {
- for ( int i = 0; i < cur_size; ++i )
- {
- delete array[i];
- }
- delete [] array;
- }
- public:
- int get_cursize()
- {
- return cur_size;
- }
- void add_timer( heap_timer* timer ) throw ( std::exception )
- {
- if( !timer )
- {
- return;
- }
- if( cur_size >= capacity )
- {
- resize();
- }
- int hole = cur_size++;
- int parent = 0;
- for( ; hole > 0; hole=parent )
- {
- parent = (hole-1)/2;
- if ( timercmp( &(array[parent]->expire), &(timer->expire), <= ) )
- {
- break;
- }
- array[hole] = array[parent];
- }
- array[hole] = timer;
- }
- void del_timer( heap_timer* timer )
- {
- if( !timer )
- {
- return;
- }
- // lazy delelte
- timer->cb_func = NULL;
- }
- int top(struct timeval &time_top) const
- {
- if ( empty() )
- {
- return 0;
- }
- time_top = array[0]->expire;
- return 1;
- }
- void pop_timer()
- {
- if( empty() )
- {
- return;
- }
- if( array[0] )
- {
- delete array[0];
- array[0] = array[--cur_size];
- percolate_down( 0 );
- }
- }
- void tick()
- {
- heap_timer* tmp = array[0];
- struct timeval cur;
- gettimeofday( &cur, NULL );
- while( !empty() )
- {
- if( !tmp )
- {
- break;
- }
- if( timercmp( &cur, &(tmp->expire), < ) )
- {
- break;
- }
- if( array[0]->cb_func )
- {
- array[0]->cb_func( array[0]->user_data );
- }
- pop_timer();
- tmp = array[0];
- }
- }
- bool empty() const
- {
- return cur_size == 0;
- }
- heap_timer** get_heap_array()
- {
- return array;
- }
- private:
- void percolate_down( int hole )
- {
- heap_timer* temp = array[hole];
- int child = 0;
- for ( ; ((hole*2+1) <= (cur_size-1)); hole=child )
- {
- child = hole*2+1;
- if ( (child < (cur_size-1)) && timercmp( &(array[child+1]->expire), &(array[child]->expire), < ) )
- {
- ++child;
- }
- if ( timercmp( &(array[child]->expire), &(temp->expire), < ) )
- {
- array[hole] = array[child];
- }
- else
- {
- break;
- }
- }
- array[hole] = temp;
- }
- void resize() throw ( std::exception )
- {
- heap_timer** temp = new heap_timer* [2*capacity];
- for( int i = 0; i < 2*capacity; ++i )
- {
- temp[i] = NULL;
- }
- if ( ! temp )
- {
- throw std::exception();
- }
- capacity = 2*capacity;
- for ( int i = 0; i < cur_size; ++i )
- {
- temp[i] = array[i];
- }
- delete [] array;
- array = temp;
- }
- private:
- heap_timer** array;
- int capacity;
- int cur_size;
- };
如何用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(出现错误)才进行返回。
废话不多说,看代码最清楚:
- void timer_handler()
- {
- heap.tick();
- //setalarm();
- }
- /* tselect - select with timers */
- int tepoll_wait( int epollfd, epoll_event *events, int max_event_number )
- {
- struct timeval now;
- struct timeval tv;
- struct timeval *tvp;
- //tevent_t *tp;
- int n;
- for ( ;; )
- {
- if ( gettimeofday( &now, NULL ) < 0 )
- perror("gettimeofday");
- struct timeval time_top;
- if ( heap.top(time_top) )
- {
- tv.tv_sec = time_top.tv_sec - now.tv_sec;;
- tv.tv_usec = time_top.tv_usec - now.tv_usec;
- if ( tv.tv_usec < 0 )
- {
- tv.tv_usec += 1000000;
- tv.tv_sec--;
- }
- tvp = &tv;
- }
- else
- tvp = NULL;
- if(tvp == NULL)
- n = epoll_wait( epollfd, events, max_event_number, -1 );
- else
- n = epoll_wait( epollfd, events, max_event_number, tvp->tv_sec*1000 + tvp->tv_usec/1000 );
- if ( n < 0 )
- return -1;
- if ( n > 0 )
- return n;
- timer_handler();
- }
- }
- while( !stop_server )
- {
- number = tepoll_wait( epollfd, events, MAX_EVENT_NUMBER);
- for ( i= 0; i < number; i++ )
- {
- int fd = events[i].data.fd;
- if ( (events[i].events & EPOLLIN)&& (fd == uart_fd) )
- {
- //读取用户数据
- if( (timer_id = find_exist_timer(ip_address)) != -1)
- {
- //add to the exist timer
- heap_timer ** heap_array = heap.get_heap_array();
- heap_array[timer_id]->user_data->add_target(RSSI,target_id);
- continue;
- }
- <span style="white-space:pre"> </span>//new timer
- heap_timer *timer = new heap_timer(200);
- timer->cb_func = cb_func;
- timer->user_data = new client_data(ip_address);
- timer->user_data->add_target(RSSI,target_id);
- heap.add_timer(timer);
- }
- else if( ( fd == pipefd[0] ) && ( events[i].events & EPOLLIN ) )
- {
- //此处进行了统一信号源处理,通过双向管道来获取SIGTERM以及SIGINT的信号,在主循环中进行统一处理
- <span style="white-space:pre"> </span>char signals[1024];
- ret = recv( pipefd[0], signals, sizeof( signals ), 0 );
- if( ret == -1 )
- {
- continue;
- }
- else if( ret == 0 )
- {
- continue;
- }
- else
- {
- for( int i = 0; i < ret; ++i )
- {
- switch( signals[i] )
- {
- case SIGTERM:
- case SIGINT:
- {
- stop_server = true;
- }
- }
- }
- }
- }
- }
- }