skynet源码分析(7)--skynet中的timer

作者:shihuaping0918@163.com,转载请注明作者
skynet的timer是做游戏用得比较频繁的一个功能,分析一下它的源码还是有意义的。而且核心的C源码除了timer和网络以外,已经基本分析得差不多了。其它都是跟lua c api相关,或者是跟lua交互比较多的。timer的源码在skynet-timer.c和skynet-timer.h中。

在开始看代码之前,请大家默念三遍:
1秒=1000毫秒
1毫秒=1000微秒
1微秒=1000纳秒

然后再默念三遍:
skynet中的时间精度是10ms级别。

如果念完了我们开始准备看代码,在看源码前,还要说明一下,skynet的外部定时器是分为两部分存的,一部分存在叫near的数组里,另一部分存在一个二维数组里,分为四个级别。

最后,如果对位运算不熟悉的话就请回吧。

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)

//先搞清楚下面的单位
//1秒=1000毫秒 milliseconds
//1毫秒=1000微秒 microseconds
//1微秒=1000纳秒 nanoseconds

//整个timer中毫秒的精度都是10ms,
//也就是说毫秒的一个三个位,但是最小的位被丢弃

struct timer_event {
    uint32_t handle; //即是设置定时器的来源,又是超时消息发送的目标
    int session; //session,一个增ID,溢出了从1开始,所以不要设时间很长的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[4][TIME_LEVEL]; //四个级别的定时器数组
    struct spinlock lock; //自旋锁
    uint32_t time; //计数器
    uint32_t starttime; //程序启动的时间点,timestamp,秒数
    uint64_t current; //从程序启动到现在的耗时,精度10毫秒级
    uint64_t current_point; //当前时间,精度10毫秒级
};

static struct timer * TI = NULL;

//清空链表,返回链表第一个结点
static inline struct timer_node *
link_clear(struct link_list *list) {
    struct timer_node * ret = list->head.next;
    list->head.next = 0;
    list->tail = &(list->head);

    return ret;
}

//将结点放入链表
static inline void
link(struct link_list *list,struct timer_node *node) {
    list->tail->next = node;
    list->tail = node;
    node->next=0;
}

//添加一个定时器结点
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 {  //这里有一种特殊情况,就是当time溢出,回绕的时候
        int i;
        uint32_t mask=TIME_NEAR << TIME_LEVEL_SHIFT;
        for (i=0;i<3;i++) { //看到i<3没,很重要很重要
            if ((time|(mask-1))==(current_time|(mask-1))) {
                break;
            }
            mask <<= TIME_LEVEL_SHIFT; //mask越来越大
        }
        //放入数组中
        link(&T->t[i][((time>>(TIME_NEAR_SHIFT + i*TIME_LEVEL_SHIFT)) & TIME_LEVEL_MASK)],node);    
    }
}

//添加一个定时器
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);

    SPIN_LOCK(T);

        node->expire=time+T->time; //超时时间+当前计数
        add_node(T,node);

    SPIN_UNLOCK(T);
}

//移动某个级别的链表内容
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 == 0) { //time溢出了
        move_list(T, 3, 0); //这里就是那个很重要的3
    } else { //time正常
        uint32_t time = ct >> TIME_NEAR_SHIFT;
        int i=0;

        while ((ct & (mask-1))==0) {
            int idx=time & TIME_LEVEL_MASK;
            if (idx!=0) {
                move_list(T, i, idx);
                break;              
            }
            mask <<= TIME_LEVEL_SHIFT; //mask越来越大
            time >>= TIME_LEVEL_SHIFT; //time越来越小
            ++i;
        }
    }
}

//派发消息到目标服务消息队列
static inline void
dispatch_list(struct timer_node *current) {
    do {
        struct timer_event * event = (struct timer_event *)(current+1);
        struct skynet_message message;
        message.source = 0;
        message.session = event->session; //这个很重要,接收侧靠它来识别是哪个timer
        message.data = NULL;
        message.sz = (size_t)PTYPE_RESPONSE << MESSAGE_TYPE_SHIFT;

        //派发定显示器消息
        skynet_context_push(event->handle, &message);
        
        struct timer_node * temp = current;
        current=current->next;
        skynet_free(temp);  
    } while (current); //一直到清空为止
}

//派发消息
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);
}

//创建一个定时器,没什么可说的
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;
}

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;
}

// centisecond: 1/100 second
static void
systime(uint32_t *sec, uint32_t *cs) {
#if !defined(__APPLE__)
    struct timespec ti;
    clock_gettime(CLOCK_REALTIME, &ti);
    *sec = (uint32_t)ti.tv_sec;
    *cs = (uint32_t)(ti.tv_nsec / 10000000);
#else
    struct timeval tv;
    gettimeofday(&tv, NULL);
    *sec = tv.tv_sec; //1970/1/1到现在的秒数
    *cs = tv.tv_usec / 10000; //微秒转毫秒,精度10ms
#endif
}

static uint64_t
gettime() {
    uint64_t t;
#if !defined(__APPLE__)
    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; //精度为10毫秒级
#endif
    return t;
}

//在线程中不断被调用
//调用时间 间隔为 2500微秒,即2.5毫秒
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); //注意这里
        }
    }
}

//返回启动的时的timestamp
uint32_t
skynet_starttime(void) {
    return TI->starttime;
}

//返回耗时
uint64_t 
skynet_now(void) {
    return TI->current;
}

//初始化
void 
skynet_timer_init(void) {
    TI = timer_create_timer();
    uint32_t current = 0;
    systime(&TI->starttime, &current); //取starttime和current
    TI->current = current;
    TI->current_point = gettime();
}

// for profile

#define NANOSEC 1000000000
#define MICROSEC 1000000

uint64_t
skynet_thread_time(void) {
#if  !defined(__APPLE__)
    struct timespec ti;
    clock_gettime(CLOCK_THREAD_CPUTIME_ID, &ti);

    return (uint64_t)ti.tv_sec * MICROSEC + (uint64_t)ti.tv_nsec / (NANOSEC / MICROSEC);
#else
    struct task_thread_times_info aTaskInfo;
    mach_msg_type_number_t aTaskInfoCount = TASK_THREAD_TIMES_INFO_COUNT;
    if (KERN_SUCCESS != task_info(mach_task_self(), TASK_THREAD_TIMES_INFO, (task_info_t )&aTaskInfo, &aTaskInfoCount)) {
        return 0;
    }

    return (uint64_t)(aTaskInfo.user_time.seconds) + (uint64_t)aTaskInfo.user_time.microseconds;
#endif
}

代码分析完了,skynet中有一个timer线程,每2.5毫秒就更新一下timer中的时间。每次更新都会对一个叫time的计数器做加1操作,所以这个计数器其实可以当作时间来看待,然后对near数组中的定时器进行触发。

前面讲到还有四个级别的定时器数组,这些数组在timer_shift中被不断地重新调整优先级,直到移动到near数组中。四个级别分别是0,1,2,3,级别越大,expire也就越大,也就是超时时间越大。timer_shift会对各个级别中的定时器做重新的级别分配。

time计数器是有可能溢出的,有人称它为回绕,当它溢出了以后,会做一个特殊处理。即对level3的整个数组进行重新级别分配。

2017.8.24
还是补充说明一下,位运算在timer四个级别中的作用吧。一个time32位,初始near使用低7位作掩码,然后是13位掩码,然后19位,然后25位。

time和current_time都是对mask做与操作,与操作就是0 | 0 = 0 其它的都为1,这个操作意味着什么呢,就意味着a | b,结果肯定是两者中最大的,或者比两个都要大。因为a,b中只要有一个对应位置分别是0,1,最终都会变成1。而mask-1所有的位都是1,所以取到的值在mask位的肯定全为1。near取的是后8位,后8位的值也就是256,也就是说如果time和current_time只差255,那么时间就真的很接近了,255*2.5毫秒不到一秒钟。mask位数越多,被mask放大的数字也就越大。当然最高位也必须是相等的才行。

为什么time溢出了要把t[3]的定时器重新分配呢?这是因为time和current_time都特别大的时候,低8位(near),8+6,8+62,这三个值的位数全设成1,不能够让time|mask等于current_time|mask。反而是8+63能够让time|mask==current_time|mask。所以全放到t[3]里面。而当time溢出了以后,current_time也会变。所以它要把t[3]全放进near里。换句话说,就是t[3]在timer溢出之前实际上充当了near的角色!

希望补充的这一段能够让多数人明白,这个算法的原理。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • https://nodejs.org/api/documentation.html 工具模块 Assert 测试 ...
    KeKeMars阅读 6,297评论 0 6
  • 简介 skynet中提供了一个简单的计时器实现,可以设置一个超时时间,时间到达后给对应的服务发送消息,这篇文字主要...
    suzuiyue阅读 1,103评论 0 0
  • 我记得太阳 似乎每天都一样 我记得月亮 好像时圆时半弯 我也记得你 一直在为事业忙碌 我也没忘记 每天翻你的日志 ...
    象天之性阅读 238评论 0 2
  • 谁说每篇文字拿出来都需要一个明确的主题?都要分一二三点来正经论述? 不,我想说,臣妾做不到啊…… 所以吧,我觉得根...
    俏村姑阅读 306评论 0 3