游戏人生:1时钟和定时器
utils 定时器 (一) 多级时间轮
utils 定时器 (二) 链表
utils 定时器 (三) 最小堆
utils 定时器 (四) 红黑树
小到游戏各种活动的定时开启,大到游戏本身就是处于一个大的主循环中,每个tick(时钟周期)做固定的事情,游戏的运行离不开时钟和定时器。
时间轮算法(Timing-Wheel):类比时钟的24时 60分 60秒的3个度量,游戏里的32位Tick可以分为 6 6 6 6 8五个度量
1 1 1 1 1 1 |1 1 1 1 1 1 |1 1 1 1 1 1 |1 1 1 1 1 1 |1 1 1 1 1 1 1 1|
5级 |4 |3 |2 |1
随着时间的推进,tick会变化,00000001 - 11111111 即从1 到 255,经历了256个tick,其中每个tick都会有对应的事件发生。至于1s 等于多少个tick是可以人为设定的,一般游戏1s=50tick,即服务器每20ms完成一次主循环。
其中把每个tick将要发生的事件用list串联起来
相比红黑树、最小堆、链表等实现,时间轮定时器的操作粒度非常小,o(1),删除的话lazy delete可以达到o(1)
基本结构
/*
* per-CPU timer vector definitions:
*/
#define TVR_BITS (8) //1级时间轮8位
#define TVN_BITS (6) //n级时间轮6位
#define TVR_SIZE (1 << TVR_BITS) //1级时间轮的刻度256格
#define TVN_SIZE (1 << TVN_BITS) //n级时间轮的刻度64格
#define TVR_MASK (TVR_SIZE - 1) //1级掩码:11111111
#define TVN_MASK (TVN_SIZE - 1) //n级掩码:111111
typedef unsigned long long mid_t;
typedef std::list<mid_t> timer_list;
typedef std::list<mid_t>::iterator timer_list_itr;
struct TVEC_TOOR_T
{
timer_list vec[TVR_SIZE]; //256条链表
};
struct TVEC_T
{
timer_list vec[TVN_SIZE]; //64
};
所以某一时刻的服务器时钟看上去是这样的。轮子级别越高,越泛泛。
第一级时间轮
1 do1-->do2-->do3--> XX00000001tick要做的事
2 doa-->dob-->doc--> XX00000010tick要做的事
... 当前tick
255 doA-->doB-->doC-->
__________
第二级时间轮
1 do1-->do2-->do3--> 当前tick的256-256*2个tick后需要做的事
2 doa-->dob-->doc-->
...
63 doA-->doB-->doC-->
__________
......
__________
第五级时间轮
1
2
...
63
游戏GM指令调整时间的需求,使得服务器时间和自然时间有差别
/* 定义时间轮类
* 在此先明确两个概念
* [自然时间]:外部系统的时间,通过time()获得
* [服务器时间]:服务器维护的系统时间,通过配置文件调整
*/
struct TIMER_VEC_BASE
{
int32 cur_tick; // 当前时钟节拍,若小于get_current_tick(),则++,每加一次查找对应hash项
s64 start_sec; // 服务器启动的[自然时间](单位秒)
s64 deviation_sec; // 服务器启动的[服务器时间]和[自然时间]之间的差值(单位秒)
s64 time_sec; // 当前[自然时间](单位秒)
s64 time_usec; // 当前[自然时间](单位微秒)
TVEC_TOOR_T tv1; // 5级hash表
TVEC_T tv2;
TVEC_T tv3;
TVEC_T tv4;
TVEC_T tv5;
s32 num; // 当前定时器数目
TIMEOUTFUNC timeout_funs[TIMEOUT_END] = { nullptr };
}
//list存放回调函数的节点 | 即tick时间后,要做的事情,需要该节点告诉我们
typedef void(*TIMEOUTFUNC)(void *data, size_t data_len); // 回调函数
struct TIMER_HANDLE
{
tick_t expires; // 超时tick
s32 interval; // 间隔时间
s32 repeats; // 执行次数
bool forever; // 是否永久执行
s32 funcid; // 回调函数id
//std::string timeout_func_name; // 回调函数名
TIMEOUTFUNC timeout_func; // 回调函数指针
char cb_data[TIMER_CB_DATA_MAX_LEN];
s32 data_len;
s32 list_index[2]; // 存放list信息,记录出于时间轮位置
timer_list *_list; // 回指指针,用于删除
mid_t this_mid; // mempool id号
bool isrunning; // 是否正在被处理,防止超时处理中删除自己
};
初始化时间轮类
static TIMER_VEC_BASE g_timer_base;
int timer_init(int max_timer_num)
{
int deviation_sec = 0; //偏移时间,服务器时间快一分钟即60
struct timeval tv;
gettimeofday(&tv, 0);//c库函数,返回微妙级别的时间
g_timer_base.start_sec = tv.tv_sec;
g_timer_base.deviation_sec = deviation_sec;
g_timer_base.time_sec = tv.tv_sec;
g_timer_base.time_usec = tv.tv_sec * 1000000 + tv.tv_usec;
g_timer_base.cur_tick = sec_to_tick(deviation_sec); //服务器时钟tick 从0开始算
// 把1-5级时间轮上的的256+64*4条链表统统清零
_init_tvr (&g_timer_base.tv1);
_init_tvn (&g_timer_base.tv2);
_init_tvn (&g_timer_base.tv3);
_init_tvn (&g_timer_base.tv4);
_init_tvn (&g_timer_base.tv5);
g_timer_base.num = 0;
return 0;
}
添加定时器
超时tick-当前tick得到时间间隔,判断时间轮级别
超时tick即绝对时间通过位操作确定是对应时间轮的第几条链表(位操作:&掩码)
掩码其实时利用了 x%(2 ^n)=x&(2 ^n -1)的性质,与运算要比取余快很多,所以tick的长度也大多为64 256 等指数
/**
@brief 加入定时器
@param[in] interval -- 间隔时间(以毫秒为单位)
@param[in] handler_func -- 回调函数指针
@param[in] cb_data -- 回调参数数据
@param[in] cb_len -- 回调参数数据长度 (最长不能超过TIME_CB_DATA_MAX_LEN)
@param[in] repeats -- 执行次数,TIMER_RUN_FOREVER(0)-永久执行
@retval INVALID_MID -- 失败
@retval 其他-- timer id,用于今后删除
*/
mid_t _add_timer_navi(int interval, s32 repeats, TIMEOUTFUNC handler_func, void *data, s32 data_len)
{
START_FUNC_PROFILER(_type_add_timer);
mid_t id;
TIMER_HANDLE *handle = nullptr;
if (!_add_timer_valid(interval, repeats, handler_func, data, data_len, TIMER_CB_DATA_MAX_LEN)) //检验有效性
return INVALID_MID;
// 从内存池分配TIMER_HANDLE,因为使用list存放回调函数的节点,list只记录一个内存池中的id
id = memunit_alloc(MEM_DATA_TYPE_TIMER_HDNLE);
handle = (TIMER_HANDLE *)memunit_get(id);
if (nullptr == handle)
{
ADD_TIMER_LOG(error_log, "timer handle alloc failed");
return INVALID_MID;
}
++(g_timer_base.num);
// 初始化
// 考虑是使用cur_tick还是使用get_tick_by_time
handle->expires = _get_expires_time(interval); //时钟当前tick + interval对应的tick
handle->interval = interval;
handle->repeats = repeats;
handle->forever = (repeats == TIMER_RUN_FOREVER);
handle->funcid = 0;
handle->timeout_func = handler_func;
if (data != nullptr) memcpy(handle->cb_data, (char *)data, data_len);
handle->data_len = data_len;
handle->this_mid = id;
handle->isrunning = false;
int ret = _internal_add_timer(handle);
if (ret != 0)
{
ADD_TIMER_LOG(error_log, "internal add timer failed");
memunit_free(id);
return INVALID_MID;
}
return id;
}
// 根据超时tick-当前tick得到时间间隔,判断时间轮级别
// 超时tick通过位操作确定是对应时间轮的第几条链表(位操作+取掩码)
static int _internal_add_timer(struct TIMER_HANDLE *handle)
{
if (handle->expires < g_timer_base.cur_tick)
{
error_log("timer %d expires %d invalid,cur_tick %d,interval %d",
handle->funcid, handle->expires, g_timer_base.cur_tick, handle->interval);
assert_retval(0, BGERR_INVALID_ARG);
}
s32 tv, pos_in_tv;
_get_timer_list_index(handle->expires, pos_in_tv, tv);
/*
* Timers are FIFO:
*/
handle->list_index[0] = tv;
handle->list_index[1] = pos_in_tv;
_timer_set_list(handle);
handle->_list->push_back(handle->this_mid); //最末尾加入 //do1-->do2-->do3-->do? (下1个tick要做的)
return 0;
}
static void _get_timer_list_index(tick_t expires, int& i, int& j)
{
assert_retnone(expires>=g_timer_base.cur_tick);
u64 idx = expires - g_timer_base.cur_tick;// 当前tick的后idx个tick
if (idx < TVR_SIZE)
{
i = expires & TVR_MASK;//取1--7位,判断是1-255中的哪条链表
j = 1;
}
else if (idx < 1 << (TVR_BITS + TVN_BITS))
{
i = (expires >> TVR_BITS) & TVN_MASK;//取8--13位
j = 2;
}
else if (idx < 1 << (TVR_BITS + 2 * TVN_BITS))
{
i = (expires >> (TVR_BITS + TVN_BITS)) & TVN_MASK;//取14--19位
j = 3;
}
else if (idx < 1 << (TVR_BITS + 3 * TVN_BITS))
{
i = (expires >> (TVR_BITS + 2 * TVN_BITS)) & TVN_MASK;//取20-25位
j = 4;
}
else
{
/* If the timeout is larger than (1 << (TVR_BITS + 3 * TVN_BITS) on 64-bit
* architectures then we use the maximum timeout:
*/
i = (expires >> (TVR_BITS + 3 * TVN_BITS)) & TVN_MASK;//取26-31位
j = 5;
}
}
static int _timer_set_list(struct TIMER_HANDLE *handle)
{
switch (handle->list_index[0]) {
case 1 : handle->_list = g_timer_base.tv1.vec + handle->list_index[1]; break;
case 2 : handle->_list = g_timer_base.tv2.vec + handle->list_index[1]; break;
case 3 : handle->_list = g_timer_base.tv3.vec + handle->list_index[1]; break;
case 4 : handle->_list = g_timer_base.tv4.vec + handle->list_index[1]; break;
case 5 : handle->_list = g_timer_base.tv5.vec + handle->list_index[1]; break;
default : assert_retval(0, BGERR_ASSERT_DEFAULT);
}
return 0;
}
删除定时器
int del_timer(mid_t timerid)
{
TIMER_HANDLE *handle;
handle = (TIMER_HANDLE *)memunit_get(timerid);
if (nullptr == handle)
{
debug_log ("invalid id %llu", mid_to_u64(timerid));
return BGERR_NOT_FOUND;
}
// 如果正在处理,则处理完后删除
if (handle->isrunning)
{
handle->repeats = 0;
handle->forever = false;
return 0;
}
// 定时器解链,list remove效率很低,需要注意
handle->_list->remove(handle->this_mid);
// 释放资源
memunit_free(handle->this_mid);
--g_timer_base.num;
return 0;
}
时间轮步进
之前总是说某一时刻的时间轮状态,现在需要的是驱动时间轮动起来。
按照10进制来分析:
1tick时添加11tick的定时器a,因为间隔不<10tick,所以要添加到二级时间轮的1号链表
1tick时添加21tick的定时器b,因为间隔不<10tick,所以要添加到二级时间轮的2号链表
9tick时添加11tick的定时器c,因为间隔<10tick,所以要添加到一级时间轮的1号链表
9tick时添加22tick的定时器d,因为间隔不<10tick,所以要添加到二级时间轮的2号链表10tick到来时,要将二级时间轮1号链表迁移到1级时间轮中,a,c都迁移到一级时间轮的1号链表中
19tick时添加20tick的定时器e,因为间隔<10tick,所以要添加到一级时间轮的0号链表
PS: 19tick时的任何定时任务无法添加到二级时间轮的1号链表中,因为10tick< 时间间隔 <100tick时,tick>19至少也是2x,只能添加到2号链表中了20tick到来时,要将二级时间轮2号链表迁移到1级时间轮中,b被迁移到一级时间轮的1号链表,d被迁移到一级时间轮的2号链表,执行e
可知相同tick的任务,在被执行前不一定都在同一条链表上,所以在进位发生时,需要把N级时间轮的链表迁移到更低级的时间轮中
void run_timer()
{
while (get_tick_by_time() >= g_timer_base.cur_tick)
{
u32 cur_tick_prev = g_timer_base.cur_tick;
__run_timers();
u32 cur_tick_next = g_timer_base.cur_tick;
assert_noeffect(cur_tick_prev < cur_tick_next);
}
}
static inline void __run_timers()
{
struct TIMER_HANDLE *handle;
mid_t id;
timer_list *list;
s32 entry_index;
TIMEOUTFUNC func;
/**
* Cascade timers:
**/
entry_index = g_timer_base.cur_tick & TVR_MASK;
if ( !entry_index &&
(!_cascade(&g_timer_base.tv2, INDEX(0))) &&
(!_cascade(&g_timer_base.tv3, INDEX(1))) &&
!_cascade(&g_timer_base.tv4, INDEX(2)))
_cascade(&g_timer_base.tv5, INDEX(3));
// 执行超时定时器链表
list = g_timer_base.tv1.vec + entry_index;
timer_list_itr itr;
while (1)
{
if (list->empty())
{
break;
}
itr = list->begin();
id = *itr;
assert_retnone(mid_is_valid(id));
handle = (TIMER_HANDLE *)memunit_get(id);
assert_retnone(handle);
assert_retnone(handle->_list == list);
// 运行定时器
func = handle->timeout_func;
assert_retnone(func);
if (handle->repeats > 0)
{
handle->repeats--;
}
if (!handle->isrunning)
{
handle->isrunning = true;
{
__run_one_timer(func, handle);
}
}
handle->isrunning = false;
// 根据策略,是否继续设置定时器
if (handle->repeats > 0 || handle->forever)
{
// 定时器解链,先删除在加到对应的时间轮节点里面
handle->_list->erase(itr);
// 重新加入
handle->expires = _get_expires_time(handle->interval);
int ret = _internal_add_timer(handle);
assert_noeffect(ret == 0);
}
else
{
del_timer(handle->this_mid);
}
}
// 看下一个单位有没有超时
++g_timer_base.cur_tick;
}
// 进位操作
static s32 _cascade(TVEC_T *tv, s32 entry_index)
{
/* cascade all the timers from tv up one level */
timer_list *list;
struct TIMER_HANDLE *handle;
list = tv->vec + entry_index;
std::list<mid_t>::iterator itr = list->begin();
// 链表搬迁
for (; itr != list->end(); )
{
handle = (struct TIMER_HANDLE *)memunit_get(*itr);
if(handle==nullptr)
{
assert_noeffect(0);
}
else
{
// 节点搬迁
// 节点搬迁时一般都是往低级的时间轮搬,如果时间间隔特别大的tick,有可能还会添在同一链表,所以添加定时器最好用头插法,
int ret = _internal_add_timer(handle);
assert_retval(ret == 0, ret);
}
itr = list.earse(itr);
}
return entry_index;
}
单级时间轮实现
虽然简单,不变的是用时间间隔确定层级(rotation),用绝对时间确定在那条链表(那个槽位)上
#ifndef TIME_WHEEL_TIMER
#define TIME_WHEEL_TIMER
#include <time.h>
#include <netinet/in.h>
#include <stdio.h>
#define BUFFER_SIZE 64
class tw_timer;
struct client_data
{
sockaddr_in address;
int sockfd;
char buf[ BUFFER_SIZE ];
tw_timer* timer;
};
// 定时器类
class tw_timer
{
public:
tw_timer( int rot, int ts )
: next( NULL ), prev( NULL ), rotation( rot ), time_slot( ts ){}
public:
int rotation; //记录定时器在时间轮转多少圈后生效
int time_slot;//记录定时器属于时间轮上的哪个槽
void (*cb_func)( client_data* ); //cb_func
client_data* user_data; //cb_data
tw_timer* next; //便于删除,链表的节点
tw_timer* prev;
};
// 定时器容器类:单级时间轮
class time_wheel
{
public:
time_wheel() : cur_slot( 0 )
{
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 ) // timeout是时间间隔
{
if( timeout < 0 )
{
return NULL;
}
int ticks = 0;
if( timeout < TI )
{
ticks = 1;
}
else
{
ticks = timeout / TI;
}
int rotation = ticks / N; // 待插入的定时器 在 时间轮转动多少圈后触发
int ts = ( cur_slot + ( ticks % N ) ) % N; // 应该被插入到哪个槽中 时间间隔+cur_slot取得类自然时间 判断放入那个槽位
// 时间轮并没有走,走的是cur_slot, 在不同的链表间步进
tw_timer* timer = new tw_timer( rotation, ts );
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;
}
}
void tick()
{
tw_timer* tmp = slots[cur_slot];
printf( "current slot is %d\n", cur_slot );
while( tmp )
{
printf( "tick the timer once\n" );
// 定时器的rotation>0 在这一轮不会起作用
if( tmp->rotation > 0 )
{
tmp->rotation--; // 相当于1点1分,2点1分,3点1分 rotation是更高的层级
tmp = tmp->next;
}
else //
{
tmp->cb_func( tmp->user_data );
// 执行完毕定时任务后删除定时器, 可以直接调用del
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;
}
}
}
cur_slot = ++cur_slot % N; // 下次循环看哪个槽
}
private:
static const int N = 60;
static const int TI = 1;
tw_timer* slots[N];
int cur_slot;
};
#endif