高效定时器:时间轮和时间堆
来源:互联网 发布:欧阳修楷书知乎 编辑:程序博客网 时间:2024/05/20 22:30
游戏后台之高效定时器-时间轮
原文地址: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;
- }
- }
- }
- }
- }
- }
- }
- 高效定时器:时间轮和时间堆
- epoll+时间堆定时器
- 时间堆实现定时器
- 游戏后台之高效定时器-时间轮
- 高性能定时器 --- 时间堆
- 定时器和时间管理
- 时间和定时器
- 定时器和获取时间
- Linux网络编程--定时器之时间堆
- 时间轮定时器 算法作用和升序定时器比较
- 时间轮定时器-Timewheel
- Linux下时间和定时器
- Linux下时间和定时器
- Linux 下时间和定时器
- LINUX定时器和时间管理
- Linux下时间和定时器
- Linux下时间和定时器
- 时间定时器
- openstack git review 29418被墙解决办法
- Transact-SQL中自定义函数的限制
- Win7下更改Eclipse SVN插件subclipse用户
- C++错误 C1189
- WheelView
- 高效定时器:时间轮和时间堆
- Struts2 Domain Model 多个bean嵌套使用
- C++ MFC Control 控件编程
- 数制转换问题:skew
- JavaScript 数字格式化函数
- 一个检查字符串是否相同的封装函数
- mysql 用户新增 权限设置
- 嵌入式linux开机应用程序自启动
- 报表应用结构优化之数据分库存储