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中故障检测的实现.本篇主要介绍集群检测 ...
随机推荐
- Android数据存储之SQLite数据库
Android数据存储 之SQLite数据库简介 SQLite的相关知识,并结合Java实现对SQLite数据库的操作. SQLite是D.Richard Hipp用C语言编写的开源嵌入式数据库引擎. ...
- msp430项目编程40
msp430综合项目---多路温度检测系统40
- D3拖动效果
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...
- codevs——2693 上学路线(施工)
2693 上学路线(施工) 时间限制: 2 s 空间限制: 16000 KB 题目等级 : 黄金 Gold 题解 题目描述 Description 问题描述 你所在的城市街道好像一个 ...
- Flink学习(一)
Apache Flink是一个面向分布式数据流处理和批量数据处理的开源计算平台,它能够基于同一个Flink运行时,提供支持流处理和批处理两种类型应用的功能. 现有的开源计算方案,会把流处理和批处理作为 ...
- LucaCanali--SystemTap_Linux_IO
https://github.com/LucaCanali/Linux_tracing_scripts/tree/master/SystemTap_Linux_IO
- BUPT复试专题—奇偶求和(2014软件)
题目描述 给出N个数,求出这N个数,奇数的和以及偶数的和. 输入 第一行为测试数据的组数T(1<=T<=50).请注意,任意两组测试数据之间是相互独立的. 每组数据包括两行: 第一行为一个 ...
- ProFTPD配置匿名登录与文件夹訪问权限控制
对ProFTPDserver配置匿名登录. 查看配置文件proftpd.conf.默认情况下配置文件里的.匿名登录配置User和Group均为ftp. 查看/etc/passwd确认用 ...
- CodeForces 321A Ciel and Robot(数学模拟)
题目链接:http://codeforces.com/problemset/problem/321/A 题意:在一个二维平面中,開始时在(0,0)点,目标点是(a.b),问能不能通过反复操作题目中的指 ...
- SQL ORDER BY 关键字
SQL ORDER BY 关键字 ORDER BY 关键字用于对结果集进行排序. SQL ORDER BY 关键字 ORDER BY 关键字用于对结果集按照一个列或者多个列进行排序. ORDER BY ...