skynet源码阅读<4>--定时器实现
昨天和三石公聊天,他提到timer的实现原理,我当时迟疑了一下,心想timer不是系统底层时钟中断驱动上层进程/线程,累积计时实现的么?他简述了timer的实现,什么堆排序,优先级队列等,与我想象的不同。正好这两天在作skynet笔记,以前也没有留意过skynet的timer,这次干脆就看看它是怎么实现的。看了之后我明白了,我与三石公所设想的不是同一个问题。他所关心的问题其实是:框架被注册多个定时回调,如何管理并尽可能高效地触发这些回调。这里我们假设框架将定时消息抽象为timer_node,框架自身最小时间片为T,不同的timer_node按照其将要被触发的时间先后排序,那么只需要在每个时间片T到来时,找到在此时刻Tk的所有timer_node加以触发就可以了。这本质是个排序问题,三石公所述的,其实是排序的不同实现方案而已。言归正传,在分析skynet的代码之前,我们先来就其实现做个简单的说明。
假设讨论的数值范围为0~999,给定一个数N,我们可以按照以下方式组织:
首先判断N的大小在哪个层级,这里对应的是【个、十、百】共3个级别,每个级别上分别建立10个桶,以存储加入进来的数据。假设N=2,那么它应落在个位级别上的Bucket2里面;假设N=32,那么它应落在百位级别上的Bucket3里面;假设N=932,那么它应落在百位级别上的Bucket9里面。可以看到,级别越高,如果所划分的桶数不变的话,单个桶中所能容纳的元素就越多,那么从此桶查找目标元素就越耗时。
设时刻t从0开始,一个时间片为10MS。建立单独一个个位级别的集合S,把个位级别的桶都加进来。t变化时,从S中取出桶来,触发桶中的timer_node。t到10时,S中的元素使用完毕,我们从十位级别拿出桶B0来,把B0中的元素与t作比较。由于t当前已经是十位级别,B0中的元素相对于t此时已经变成个位级别,因此B0内的元素会重新添加到集合S中来。每次当t走完一个新的周期0~9,就重新这个筛选的过程,B1,B2……依次类推。而当t刚增长到百位级别时,它要从百位级别拿出桶B0,其中的元素一部分相对于t是个位级别,一部分相对于t是十位级别,于是前者被添加到S,而后者被重新筛选添加到十位级别的桶中去。在这之后,t每走完一个十位级别的周期(0~10),就要重复十位级别的重新筛选过程;当t走完一个百位级别的周期时(比如从100到200),则要取出下一个百位级别的桶,然后重复百位级别的筛选过程,依次类推。
在t不断变化的过程中,如果有新的timer_node加入进来,则计算出相对于t的级别,加入到对应的桶中去。
说完思路,来看看skynet_timer的实现。先看下数据结构的设计:
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[][TIME_LEVEL];
struct spinlock lock;
uint32_t time;
uint32_t starttime;
uint64_t current;
uint64_t current_point;
}; static struct timer * TI = NULL;
这里TI->near就是我们说的集合S,它分为TIME_NEAR(256)个桶,而TI->t则是其所建立的不同级别的集合,一共4个级别,每个级别分TIME_LEVEL(32)个桶。所以算起来一共有5个级别,第一个级别是0~7位共8位,后面每个级别是6位,所以总共是8+6*4=32位,正好把unit_32用完。TI->time就是我们说的当前时刻t了,它每次以10MS为单位增长。
看下timer_node是如何加进来的:
static void
add_node(struct timer *T, struct timer_node *node) {
uint32_t time=node->expire;
uint32_t current_time=T->time; if ((time|TIME_NEAR_MASK)==(current_time|TIME_NEAR_MASK)) {
link(&T->near[time&TIME_NEAR_MASK],node);
} else {
int i;
uint32_t mask=TIME_NEAR << TIME_LEVEL_SHIFT;
for (i=;i<;i++) {
if ((time|(mask-))==(current_time|(mask-))) {
break;
}
mask <<= TIME_LEVEL_SHIFT;
} link(&T->t[i][((time>>(TIME_NEAR_SHIFT + i*TIME_LEVEL_SHIFT)) & TIME_LEVEL_MASK)],node);
}
}
在add_node函数中,目标时刻为time(当前时间+duration),current_time为当前时刻。TIME_NEAR_MASK为255,表示最后8位,也就是级别0集合(这里假设5个级别是从0数起),(time|TIME_NEAR_MASK)==(current_time|TIME_NEAR_MASK)的意思是将current_time、time的最后8位都置1,即认为它们只在级别0上不等(全置1后级别0就完全相等了),比较它们在高级别上是否相等。如果是,则其相对级别为0,加入T->near集合,否则判断它们在哪个级别上不等:每次将mask左移TIME_LEVEL_SHIFT(6)位,再进行先|再==的比较,以判断是否在级别1、2、3……上不等。假设mask在第L级别移位后,做time与current_time的先|再==操作,成立的话说明time与current_time仅仅在级别L上是不同的,否则一直向上找,直到找到最高的不同级别LI。以【个、十、百】来比较的话,假设time为5320,current_time为5120,那么会在百位级别上发现不等(先|再==的结果为相等),即相对级别为百位级别,会将元素扔到百位级别的桶中去。再假设time为5320,current_time为20,那么做先|再==的操作,直到千位级别才能结束,即二者相对级别为千位级别,则要将元素扔到千位的桶中去。即找到最大的相对级别后,则要计算出time在这个级别属于哪个桶。首先是(time>>(TIME_NEAR_SHIFT+i*TIME_LEVEL_SHIFT))将此级别段的所有bit移到最右侧,然后&TIME_LEVEL_MASK求余,得到桶号,最后加入进去。
说完加入,下一步就要看如何执行了:
static void
move_list(struct timer *T, int level, int idx) {
struct timer_node *current = link_clear(&T->t[level][idx]);
while (current) {
struct timer_node *temp=current->next;
add_node(T,current);
current=temp;
}
} static void
timer_shift(struct timer *T) {
int mask = TIME_NEAR;
uint32_t ct = ++T->time;
if (ct == ) {
move_list(T, , );
} else {
uint32_t time = ct >> TIME_NEAR_SHIFT;
int i=; while ((ct & (mask-))==) {
int idx=time & TIME_LEVEL_MASK;
if (idx!=) {
move_list(T, i, idx);
break;
}
mask <<= TIME_LEVEL_SHIFT;
time >>= TIME_LEVEL_SHIFT;
++i;
}
}
} static inline void
timer_execute(struct timer *T) {
int idx = T->time & TIME_NEAR_MASK; while (T->near[idx].head.next) {
struct timer_node *current = link_clear(&T->near[idx]);
SPIN_UNLOCK(T);
// dispatch_list don't need lock T
dispatch_list(current);
SPIN_LOCK(T);
}
} static void
timer_update(struct timer *T) {
SPIN_LOCK(T); // try to dispatch timeout 0 (rare condition)
timer_execute(T); // shift time first, and then dispatch timer message
timer_shift(T); timer_execute(T); SPIN_UNLOCK(T);
}
timer_update中先execute,根据当前时刻TI->time取出TI->near中timer_node并向目标分发消息。然后做关键的timer_shift,当前时刻TI->time加1,此时就要判断它是否处于不同级别的周期临界上,从上面的说明我们知道,当它在某个级别Ln时,它需要将Ln中下一个桶中的元素取出重新筛选到L0~Ln-1各级别中去==>
(mask-1)的初始值是(TIME_NEAR-1)(255,级别0范围),而ct(++TI->time,当前时间)与(mask-1)做&操作,也就是在级别0范围内求余,如果不为0的话,说明ct是在级别0内增加的,比如从2-->3。反之则说明当前时间ct在大于0的级别。在此情况下,time初值已经是ct>>TIME_NEAR_SHIFT了,其与TIME_LEVEL_MASK做&操作,即在级别1范围内求余。如果为0的话说明在大于1的级别,跳出;否则mask继续左移TIME_LEVEL_SHIFT以扩大级别范围,time则继续右移TIME_LEVEL_SHIFT在新级别内求余。当余数idx不为0时,表明ct在这个级别内增加了(比如从299->300,以【个、十、百】比较的话),那么此时就要取出这个level级别的桶idx内的元素重新筛选,根据相对于ct的级别重新分配到低级别的桶中去。move_list所做的,便是这个重新分配的过程。
至此,算法的详细过程已经分析完毕了。其思想是只关注较近时间段内的timer_node排序,限制了每次处理的最小集合。而实际使用定时器,一般是时间小的定时器居多,时间越大,这种定时器实际使用的情况越少,因此在高级别桶内的元素数目不会很多,重新筛选分配的开销也不大。最后,再看看在skynet_start.c中,是如何驱动skynet_timer的:
static void *
thread_timer(void *p) {
struct monitor * m = p;
skynet_initthread(THREAD_TIMER);
for (;;) {
skynet_updatetime();
CHECK_ABORT
wakeup(m,m->count-);
usleep();
}
11 // other
... }
可以看到,是开启了一个单独的线程,每隔2500微秒(2.5毫秒)来驱动的。
skynet源码阅读<4>--定时器实现的更多相关文章
- skynet源码阅读<1>--lua与c的基本交互
阅读skynet的lua-c交互部分代码时,可以看到如下处理: struct skynet_context * context = lua_touserdata(L, lua_upvalueindex ...
- skynet源码阅读<3>--网关分析
继上一篇介绍了skynet的网络部分之后,这一篇以网关gate.lua为例,简单分析下其串接和处理流程. 在官方给出的范例中,是以examples/main.lua作为启动脚本的,在此过程中会创建wa ...
- skynet 源码阅读笔记 bootstrap.lua
最近几周粗略看了 skynet 代码的 C 部分.遇到很多知识点以前只是知道,但并不十分了解,所以这是一个学习的过程. 从 main 函数开始,闷头一阵看下来,着实蛋疼. 当看了 skynet_mq. ...
- skynet源码阅读<5>--协程调度模型
注:为方便理解,本文贴出的代码部分经过了缩减或展开,与实际skynet代码可能会有所出入. 作为一个skynet actor,在启动脚本被加载的过程中,总是要调用skynet.start和sky ...
- skynet源码阅读<7>--死循环检测
在使用skynet开发时,你也许会碰到类似这样的警告:A message from [ :0100000f ] to [ :0100000a ] maybe in an endless loop (v ...
- skynet源码阅读<6>--线程调度
相比于上节我们提到的协程调度,skynet的线程调度从逻辑流程上来看要简单很多.下面我们就来具体做一分析.首先自然是以skynet_start.c为入口: static void start(int ...
- skynet源码阅读<2>--网络部分
先来看下socket_server的数据结构,这里简称为ss: struct socket_server { int recvctrl_fd; int sendctrl_fd; int checkct ...
- 【原】AFNetworking源码阅读(一)
[原]AFNetworking源码阅读(一) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 AFNetworking版本:3.0.4 由于我平常并没有经常使用AFNetw ...
- Redis源码阅读(六)集群-故障迁移(下)
Redis源码阅读(六)集群-故障迁移(下) 最近私人的事情比较多,没有抽出时间来整理博客.书接上文,上一篇里总结了Redis故障迁移的几个关键点,以及Redis中故障检测的实现.本篇主要介绍集群检测 ...
随机推荐
- js中window.location.search的用法和作用
用该属性获取页面 URL 地址: window.location 对象所包含的属性 属性 描述 hash 从井号 (#) 开始的 URL(锚) host 主机名和当前 URL 的端口号 hostnam ...
- 关于xshell无法连接到centos的问题
1.xshell无法连接到centos:拒绝连接(无线网) 在xshell ping centos出现: 解决方法: 1. 2.重启下网卡: [root@localhost ~]# /etc/init ...
- Spring整合SSM的配置文件详解
在整合三大框架SSM , 即 Spring 和 SpingMVC和Mybatis的时候,搭建项目最初需要先配置好配置文件. 有人在刚开始学习框架的时候会纠结项目搭建的顺序,因为频繁的报错提示是会很影响 ...
- C/C++动态二维数组的内存分配和释放
C语言: 1 //二维数组动态数组分配和释放 //数组指针的内存分配和释放 //方法一 char (*a)[N];//指向数组的指针 a = (char (*)[N])malloc(sizeof(ch ...
- A002-开发工具介绍
关于Android的开发工具有非常多,基本上都能够在SDK中找到.下面我们逐个来看一下: 首先我们使用的是Java语言进行Android应用的开发,那么Java的执行环境是少不了的了,我们须要在我们的 ...
- PS 图层后面有索引两字怎么办
ps中图层后面有索引两字的怎么把它拖进别的图中?或怎么把索引去掉? 悬赏分:0 | 解决时间:2010-11-5 08:58 | 提问者:jk500pk 最佳答案 图像--模式 把索引颜色模式改成RG ...
- JAVA设计模式之单例模式(转)
本文继续介绍23种设计模式系列之单例模式. 概念: java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍三种:懒汉式单例.饿汉式单例.登记式单例. 单例模式有以下特点: 1.单 ...
- 【转载】关于C#静态构造函数的几点说明
一.定义 静态构造函数是C#的一个新特性,其实好像很少用到.不过当我们想初始化一些静态变量的时候就需要用到它了.这个构造函数是属于类的,而不是属于哪里实例的,就是说这个构造函数只会被执行一次.也就是在 ...
- webform的操作完之后返回主页面的行定位
1.在repeater表格的行绑定时给行一个id(唯一id),此地方为绑定该表格的主键. watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdTAxMDA3OD ...
- [转载]C函数的实现(strcpy,atoi,atof,itoa,reverse)
在笔试面试中经常会遇到让你实现C语言中的一些函数比如strcpy,atoi等 1. atoi 把字符串s转换成数字 int Atoi( char *s ) { , i = ; ; ; isspace( ...