浅析时间轮定时器

前言: 最早是看到skynet群里边有人问如何取消定时器的问题,那时候正好在研读skynet代码,于是决定试试。但是最终只在lua层面实现了一个伪取消定时器的方案,而且还是不是优解。

    云风说从c层面取消定时器的开销要大于从lua层面取消的开销,当时不知道为什么。

    最近研读了云风实现的时间轮定时器代码(看着相当费劲啊),  过程中网上搜了很多资料,但大部分没能帮助我有个更好的理解,所以打算从写篇文章,希望能帮助像我一样的newbee, 更好的理解时间轮定时器。

    这里不讲定时器的进化过程,只讲时间轮,以及 skynet 中云风十分精巧的实现(才疏学浅,看这块儿代码,真他妈的爽)

步骤: 1 创建时间轮 2 添加到期时间 3 时钟tick 执行定时器到期回调,移动定时器列表  循环往复

1.初始化 "时间轮"

  首先看下相关的数据结构

typedef void (*timer_execute_func)(void *ud,void *arg);

#define TIME_NEAR_SHIFT 8
#define TIME_NEAR (1 << TIME_NEAR_SHIFT)
#define TIME_LEVEL_SHIFT 6
#define TIME_LEVEL (1 << TIME_LEVEL_SHIFT)
#define TIME_NEAR_MASK (TIME_NEAR-1)
#define TIME_LEVEL_MASK (TIME_LEVEL-1)

struct timer_event {
 uint32_t handle;
 int session;
};

struct timer_node {
 struct timer_node *next;
 uint32_t expire;
};

struct link_list {
 struct timer_node head;
 struct timer_node *tail;
};

struct timer {
 struct link_list near[TIME_NEAR];
 struct link_list t[4][TIME_LEVEL];
 struct spinlock lock;
 uint32_t time;
 uint32_t starttime;
 uint64_t current;
 uint64_t current_point;
};

可以看出 struct timer 就是 时间轮了。ok 下面是初始化代码

static struct timer * TI = NULL;

static struct timer *
timer_create_timer() {
 struct timer *r=(struct timer *)skynet_malloc(sizeof(struct timer));
 memset(r,0,sizeof(*r));

int i,j;

for (i=0;i<TIME_NEAR;i++) {
  link_clear(&r->near[i]);
 }

for (i=0;i<4;i++) {
  for (j=0;j<TIME_LEVEL;j++) {
   link_clear(&r->t[i][j]);
  }
 }

SPIN_INIT(r)

r->current = 0;

return r;
}

static uint64_t
gettime() {
 uint64_t t;
#if !defined(__APPLE__) || defined(AVAILABLE_MAC_OS_X_VERSION_10_12_AND_LATER)
 struct timespec ti;
 clock_gettime(CLOCK_MONOTONIC, &ti);
 t = (uint64_t)ti.tv_sec * 100;
 t += ti.tv_nsec / 10000000;
#else
 struct timeval tv;
 gettimeofday(&tv, NULL);
 t = (uint64_t)tv.tv_sec * 100;
 t += tv.tv_usec / 10000;
#endif
 return t;
}

void
skynet_timer_init(void) {
 TI = timer_create_timer();
 uint32_t current = 0;
 systime(&TI->starttime, &current);
 TI->current = current; //初始化时刻,纳秒数
 TI->current_point = gettime();
}

首先调用 timer_create_time() 创建五个数组  一个struct link_list near[TIME_NEAR] TIME_NEAR, 四个 struct link_list t[4][TIME_LEVEL]; TIME_LEVEL, 每个数组的每个 slot 表示一个时间段 同时又是个链表,用来存储

到期时间距当前tick 一个时间段的定时器。 skynet 提供的定时器精度为 1/100 秒 也就是10毫秒, 具体实现为 gettime() 。既然精度是 10 毫秒 那么 10毫秒就要调用一次  dispatch 函数,触发定时器,

那么哪里调用的呢?

答案在skynet_start.c里 其中定时器线程

static void *
thread_timer(void *p) {
 struct monitor * m = p;
 skynet_initthread(THREAD_TIMER);
 for (;;) {
  skynet_updatetime();
  CHECK_ABORT
  wakeup(m,m->count-1);
  usleep(2500);
  if (SIG) {
   signal_hup();
   SIG = 0;
  }
 }
 // wakeup socket thread
 skynet_socket_exit();
 // wakeup all worker thread
 pthread_mutex_lock(&m->mutex);
 m->quit = 1;
 pthread_cond_broadcast(&m->cond);
 pthread_mutex_unlock(&m->mutex);
 return NULL;
}

撇开无关代码, 可以提取出

void
skynet_updatetime(void) {
 uint64_t cp = gettime();
 if(cp < TI->current_point) {
  skynet_error(NULL, "time diff error: change from %lld to %lld", cp, TI->current_point);
  TI->current_point = cp;
 } else if (cp != TI->current_point) {
  uint32_t diff = (uint32_t)(cp - TI->current_point);
  TI->current_point = cp;
  TI->current += diff;
  int i;
  for (i=0;i<diff;i++) {
   timer_update(TI);
  }
 }
}

这里基本可以保证diff 的值为1, 也就是10 毫秒的时间间隔, 可以写个测试程序试一下,稍后贴运行结果

for (;;) {
  skynet_updatetime();
  usleep(2500);
  }
 }

ok 现在我们创建了定时器的骨架, 然后也知道了在哪里保证 10 毫秒触发定时器,如果没有注册定时器,就是空转。

个人觉得时间轮定时器的难点在于 注册时 和 shift 时,定位到slot。要想知道这点,一个好的方法就是

现在我们注册几个定时器。 这里会挑一些时间点的定时器,来分析注册,分发,shift 过程。

先看注册函数

int
skynet_timeout(uint32_t handle, int time, int session) {
 if (time <= 0) {
  struct skynet_message message;
  message.source = 0;
  message.session = session;
  message.data = NULL;
  message.sz = (size_t)PTYPE_RESPONSE << MESSAGE_TYPE_SHIFT;

if (skynet_context_push(handle, &message)) {
   return -1;
  }
 } else {
  struct timer_event event;
  event.handle = handle;
  event.session = session;
  timer_add(TI, &event, sizeof(event), time);
 }

return session;
}

timer_add(TI, &event, sizeof(event), time);

看看timer_add;

static void
timer_add(struct timer *T,void *arg,size_t sz,int time) {
 struct timer_node *node = (struct timer_node *)skynet_malloc(sizeof(*node)+sz);
 memcpy(node+1,arg,sz);  //每个node后边绑一个event参数

SPIN_LOCK(T);

node->expire=time+T->time;
  add_node(T,node);

SPIN_UNLOCK(T);
}

申请一个node,看 node->expire = time+T->time; 到期时间是 time+T->time,   那么T->time 是啥,这个值是如何变化的,在哪里变化的

现需要留神下几个变量的含义

T->time; T->starttime; T->current; T->current_point;

T->time 服务器经过的tick 数, 每10毫秒 tick 一次,T->time 增加1;

T->starttime; 服务器开始的时间,单位秒。

T->current (uint32_t)(ti.tv_nsec / 10000000);

T->current_point   t = (uint64_t)ti.tv_sec * 100, t += ti.tv_nsec / 10000000 ;

    

  

原文地址:https://www.cnblogs.com/newbeeyu/p/9022623.html

时间: 2024-08-06 11:19:22

浅析时间轮定时器的相关文章

记录——时间轮定时器(lua 实现)

http://www.cnblogs.com/mmc1206x/p/6849172.html 很长一段时间里,我错误的认识了定时器.无意中,我发现了“时间轮”这个名词,让我对定时器有了新的看法. 我错误的认为,定时器只需要一个 tick 队列,按指定的时间周期遍历队列,检查 tick 倒计时满足触发条件就触发回调. tick 定义如下: 1 struct Tick { 2 int_t n; 3 func_t func; 4 }; 遍历触发实现如下: 1 void Update() 2 { 3 f

原 荐 简单说说Kafka中的时间轮算法

零.时间轮定义 简单说说时间轮吧,它是一个高效的延时队列,或者说定时器.实际上现在网上对于时间轮算法的解释很多,定义也很全,这里引用一下 朱小厮博客 里出现的定义: 参考下图,Kafka中的时间轮(TimingWheel)是一个存储定时任务的环形队列,底层采用数组实现,数组中的每个元素可以存放一个定时任务列表(TimerTaskList).TimerTaskList是一个环形的双向链表,链表中的每一项表示的都是定时任务项(TimerTaskEntry),其中封装了真正的定时任务TimerTask

高性能定时器时间轮的探究

时间轮的概念 关于定时器有很多种,有基于升序的定时器时间链表,但是这种链表存在效率的不足,就是当插入定时器的时候时间复杂度是O(n).今天,我们来认识一下高性能定时器时间轮. 如上图所示,就是一个时间轮的基本轮廓.一个轮子上有很多槽slot,每一个槽指向一个定时器链表,这个链表是无序的.时间轮每转动一步就会指向下一个槽,其实也可以理解为一个滴答时间成为时间轮的槽间隔si (slot interval).它实际上就是心跳时间.如果该时间轮有n个槽,因此它运转一周的时间是n*si. 如果现在指针指向

时间轮算法

问题引入:游戏里面每个Player身上有很多buffs,在每一个tick(最小时间段)都要去检查buff里面的每一个buff是不是过期,产生的效果如何,造成在每个tick里面都去遍历一个长list,明显很不好. 怎么优化? 1.原始模型:    buff的状态在每一个tick里面都要更新!可以想象指针每移动一下,都会非常沉重地拖着所有的BuffList,好可怕-- 2. 优化模型1:   我们要避免的是:原始模型在每一个tick里面都要遍历List,那么我们试下以Times为key,在加入buf

时间轮

老早之前就听说时间轮算法特别高效,Linux内核都用的它,这两天抽空实现了遍--嗯,被差一bug搞死(~ ̄▽ ̄~) 啊哈 网上扣来的图,原理好懂:轮子里的每格代表一小段时间(精度),连起来就能表示时间点了(我去年买了个表),格子内含链表,中存回调函数:时间指针每次转动一格,指向某格时,取出链表里的回调函数依次执行,后清空链表,等待下一次转动. 加入节点逻辑也简单:在轮子可表示的时间范围内(格子数*格子精度),配合当前时针位置,对格子总数取余,即得本节点需放哪个格子. 进一步为扩大时间轮的表示范围

Linux C++ 实现时间轮 优化超时检测机制

参考资料: http://www.ijilei.com/8357 https://www.zhihu.com/question/38427301 https://www.ibm.com/developerworks/cn/linux/l-cn-timers/ http://www.cnblogs.com/processakai/archive/2012/04/11/2442294.html 思路和代码的编写主要是参考的csdn上的一个java的代码 http://blog.csdn.net/mi

时间轮(TimeWheel)的设计与实现

一.前言 由于工作的需要,得实现一个用于控制事件超时抛弃的时间轮,由于这是一个相对独立的接口,就总结分享一下. 首先看下需求,此时间轮需要具备下面几个功能: 1)能添加事件,同时附上其超时时间: 2)如果事件正常执行结束,可以显示将其从时间轮上剔除掉,而不需要等时间轮自动移除: 3)如果事件到了设定的超时时间还没执行完,则时间轮需将其剔除掉,并发送一个超时的消息给系统. 基于这样的需求,下面就进行相应的设计和实现. 二.时间轮的设计 基于前面的需求,可以抽象出两个实体来:时钟和槽,其中时钟去负责

Linux信号实践(5) --时间与定时器

三种不同精度的睡眠 1.sleep #include <unistd.h> unsigned int sleep(unsigned int seconds); RETURN VALUE Zero if the requested time has elapsed, or the number of seconds left to  sleep, if  the call was interrupted by a signal handler. //示例 int sleepTime = 5; d

linux时间和定时器zz

https://www.cnblogs.com/cobbliu/p/3627061.html Linux 的计时函数,用于获得当前时间: time(2) / time_t (秒) ftime(3) / struct timeb (毫秒) gettimeofday(2) / struct timeval (微秒) clock_gettime(2) / struct timespec (纳秒) gmtime / localtime / timegm / mktime / strftime / str