游戏人生: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级掩码:111111typedef 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;     // 超时ticks32 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; //偏移时间,服务器时间快一分钟即60struct 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只记录一个内存池中的idid = 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_timehandle->expires = _get_expires_time(interval); //时钟当前tick + interval对应的tickhandle->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个tickif (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_funcclient_data* user_data;          //cb_datatw_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 );// 执行完毕定时任务后删除定时器, 可以直接调用delif( 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

utils 定时器 (一) 多级时间轮相关推荐

  1. 高性能定时器--时间轮/多级时间轮

    运行原理 指针指向轮子上的一个槽,轮子以恒定的速度顺时针转动,每转动一步就指向下一个槽(虚线指针指向的槽),每次转动称为一个tick,一个tick的时间称为时间轮的槽间隔slot interval,即 ...

  2. linux编程之经典多级时间轮定时器(C语言版)

    一. 多级时间轮实现框架 上图是5个时间轮级联的效果图.中间的大轮是工作轮,只有在它上的任务才会被执行:其他轮上的任务时间到后迁移到下一级轮上,他们最终都会迁移到工作轮上而被调度执行. 多级时间轮的原 ...

  3. 网络编程 高性能定时器数据结构分析 | 时间轮 红黑树定时器性能分析 | 为什么要做用户态定时器

    为什么要用户态的定时器? 首先是为什么要做定时器,定时器的主要说的是我们的应用(业务?功能?总之有这个需求)要做一个定时的任务.其实如果不想为什么,好像是理所当然的.我写这个的时候,知乎有一个问题(L ...

  4. 多线程环境下海量定时任务的定时器设计丨时间轮实现丨红黑树,跳表分析

    多线程环境下海量定时任务定时器设计 1. 定时器分析 2. 红黑树,最小堆,跳表实现比较分析 3. 时间轮实现 [Linux后端开发系列]多线程环境下海量定时任务的定时器设计丨时间轮实现丨红黑树,跳表 ...

  5. jQuery 一次定时器_干货 | 小论定时器玩法(时间轮询法)

    EEWORLD 电子资讯 犀利解读 技术干货 每日更新 经常来说,对于一些不复杂的单片机应用,而且对于内存和存储要求比较严格,又需要多分时去处理一些指定的任务,在无法使用RTOS的情况下,使用一个硬件 ...

  6. 心跳超时时间设置_定时器实现之时间轮算法

    前言 在看这篇文章的时候对其中超时控制一块儿有点好奇.通过时间轮来控制超时?啥是时间轮?怎么控制的?文章会先介绍常见的计时超时处理,再引入时间轮介绍及 netty 在实现时的一些细节,最后总结下实现的 ...

  7. linux 定时器(c++)(2)时间轮

    节点类 相比较时间升序链表中的绝对时间expire tw_timer采用的是相对时间的概念也就多出 rotation ,time_slot俩属性,rotation是转的"圈数",t ...

  8. Linux网络编程 | 高性能定时器 :时间轮、时间堆

    文章目录 时间轮 时间堆 在上一篇博客中我实现了一个基于排序链表的定时器容器,但是其存在一个缺点--随着定时器越来越多,添加定时器的效率也会越来越低. 而下面的两个高效定时器--时间轮.时间堆,会完美 ...

  9. 时间轮 (史上最全)

    缓存之王 Caffeine 中,涉及到100w级.1000W级.甚至亿级元素的过期问题,如何进行高性能的定时调度,是一个难题. 注: 本文从 对 海量调度任务场景中, 高性能的时间轮算法, 做了一个 ...

  10. 孙玄辜教授:基于Linux内核的时间轮算法设计实现【附代码】

    文章目录 1.时间轮算法基本思想 2.定时器的添加 3.定时器到期处理 孙玄:毕业于浙江大学,现任转转公司首席架构师,技术委员会主席,大中后台技术负责人(交易平台.基础服务.智能客服.基础架构.智能运 ...

最新文章

  1. tf.keras.layers.Embedding 嵌入层 示例
  2. 利用List比较数组有优势吗?
  3. 数学是什么?_题跋—数学是什么?
  4. 关于python的一些好的书籍推荐-如果只能推荐3本关于python的书,你会推荐哪3本?...
  5. 利用Fiddler模拟POST请求
  6. ubuntu 电源按钮操作_桌面应用|在 Ubuntu 中使用 Slimbook Battery Optimizer 切换电源模式...
  7. Linux常用命令 一
  8. java求出遍历二叉树的路径,102. 二叉树的层序遍历
  9. 【HNOI2006】【Luogu2320】鬼谷子的钱袋(进制,玄学)
  10. C语言基础:for循环演示源码,字符循环和浮点数循环
  11. Maven插件:maven-javadoc-plugin
  12. OpenStack创业“五虎将”分化
  13. sqlnet.ora
  14. html那种折叠文字内容怎么实现,html+css实现文字折叠特效实例
  15. 苹果服务器维护时间表2019,ios 内购详解(2019)
  16. 整整26本!由单墫教授主编、葛军等人操刀的高中数学教材都在这里!
  17. 伊隆 马斯克经典语录英文_我写关于伊隆·麝香的信时叫我出去
  18. js实现点气球小游戏
  19. Yeo小程序组件库(v1.0)
  20. 推荐一款免费的数据库管理工具,比Navicat还要好用,功能还很强大

热门文章

  1. C语言程序员面试100题,c语言面试最必考的十道试题,求职必看!!!
  2. python 视频保存_通过Python保存央视频某主题的视频地址
  3. MT4跟单软件多帐户跨平台如何解决喊单账号与跟单账号个别品种合约数量不一致的问题?——Hookswork
  4. 华为手机的分类有何区别_华为手机有多少种型号,几个系列?
  5. Linux brctl 详解
  6. MIPS32-单周期数据通路设计
  7. linux异步io缺陷,具有libaio性能问题的Linux异步IO
  8. ImageJ批量操作时常见报错及其原因
  9. 单片机c语言延时30s程序,单片机c语言中的精确延时程序
  10. 使用 stress 命令对cpu进行压力测试