zoukankan      html  css  js  c++  java
  • skynet源码分析之定时器skynet_timer.c

    skynet自带定时器功能skynet-src/skynet_timer.c,在skynet启动时会创建一个线程专门跑定时器。每帧(0.0025秒/帧)调用skynet_updatetime()

     1 // skynet-src/skynet_start.c
     2 
     3 create_thread(&pid[1], thread_timer, m);
     4 
     5 static void *
     6 thread_timer(void *p) {
     7     struct monitor * m = p;
     8     skynet_initthread(THREAD_TIMER);
     9     for (;;) {
    10         skynet_updatetime();
    11         CHECK_ABORT
    12         wakeup(m,m->count-1);
    13         usleep(2500);  //2500微妙=0.0025秒
    14         if (SIG) {
    15             signal_hup();
    16             SIG = 0;
    17         }
    18     }
    19     ...
    20 }

    1. 设计思想

    skynet的设计思想参考Linux内核动态定时器的机制,参考Linux动态内核定时器介绍http://www.cnblogs.com/leaven/archive/2010/08/19/1803382.html

    在skynet里,时间精度是0.01秒,这对于游戏服务器来说已经足够了,定义1滴答=0.01秒,1秒=100滴答。其核心思想是:每个定时器设置一个到期的滴答数,与当前系统的滴答数(启动时是0,然后1滴答1滴答往后跳)比较差值,如果差值interval比较小(0<=interval<=2^8-1),表示定时器即将到来,需要严格关注,把它们保存在2^8个定时器链表里;如果interval越大,表示定时器越远,可以不用太关注,划分成4个等级,2^8<=interval<=2^(8+6)-1,2^(8+6)<=interval<=2^(8+6+6),...,每个等级只需要2^6个定时器链表保存,比如对于2^8<=interval<=2^(8+6)-1的定时器,将interval>>8相同的值idx保存在第一个等级位置为idx的链表里。

    这样做的优势是:不用为每一个interval创建一个链表,而只需要2^8+4*(2^6)个链表,大大节省了内存。

    之后,在不同情况下,分配不同等级的定时器,等级越高,表示越遥远,需要重新分配的次数越少。

    2. 源码分析

    数据结构:timer->near,保存2^8个即将到来的定时器链表;timer->t,保存4个分级数组,数组的每一项是一个链表;timer->time保存从skynet启动到现在走过的滴答数

     1 // skynet-src/skynet_timer.c
     2 struct timer_event {
     3     uint32_t handle;
     4     int session;
     5 };
     6 
     7 struct timer_node { //单个定时器节点
     8     struct timer_node *next;
     9     uint32_t expire; //到期滴答数
    10 };
    11 
    12 struct link_list { //定时器链表
    13     struct timer_node head;
    14     struct timer_node *tail;
    15 };
    16 
    17 struct timer {
    18     struct link_list near[TIME_NEAR];
    19     struct link_list t[4][TIME_LEVEL]; 
    20     struct spinlock lock;
    21     uint32_t time; //启动到现在走过的滴答数,等同于current
    22     ...
    23 };

    调用skynet_timeout创建一个定时器

    然后调用timer_add(第8行),给timer_node指针分配空间,timer_node结构里并没有timer_event字段,除了分配node自身大小外,额外再分配timer_event大小的空间用来存放event,之后通过node+1的位置可以获取到timer_event数据

    然后调用add_node(第21行),添加到定时器链表里,如果定时器的到期滴答数跟当前比较近(<2^8),表示即将触发定时器添加到T->near数组里,否则根据差值大小添加到对应的T->T[i]中

     1 // skynet-src/skynet_timer.c
     2 int
     3 skynet_timeout(uint32_t handle, int time, int session) {
     4     ...
     5     struct timer_event event;
     6     event.handle = handle;
     7     event.session = session;
     8     timer_add(TI, &event, sizeof(event), time);
     9 
    10     return session;
    11 }
    12 
    13 static void
    14 timer_add(struct timer *T,void *arg,size_t sz,int time) {
    15     struct timer_node *node = (struct timer_node *)skynet_malloc(sizeof(*node)+sz);
    16     memcpy(node+1,arg,sz);
    17 
    18     SPIN_LOCK(T);
    19 
    20     node->expire=time+T->time;
    21     add_node(T,node);
    22 
    23     SPIN_UNLOCK(T);
    24 }
    25 
    26 static void
    27 add_node(struct timer *T,struct timer_node *node) {
    28     uint32_t time=node->expire;
    29     uint32_t current_time=T->time;
    30         
    31     if ((time|TIME_NEAR_MASK)==(current_time|TIME_NEAR_MASK)) {
    32         link(&T->near[time&TIME_NEAR_MASK],node);
    33     } else {
    34         int i;
    35         uint32_t mask=TIME_NEAR << TIME_LEVEL_SHIFT;
    36         for (i=0;i<3;i++) {
    37             if ((time|(mask-1))==(current_time|(mask-1))) {
    38                  break;
    39             }
    40             mask <<= TIME_LEVEL_SHIFT;
    41         }
    42 
    43         link(&T->t[i][((time>>(TIME_NEAR_SHIFT + i*TIME_LEVEL_SHIFT)) & TIME_LEVEL_MASK)],node);        
    44     }
    45 }

    每帧从T->near中触发到期的定时器链表,near数组里每一项的链表中的所有节点的到期滴答数是相同的。

    调用dispatch_list进行分发,通过current+1获取timer_event数据(第18行),然后给event->handle push一条消息表示触发定时器(第25行)

     1 // skynet-src/skynet_timer.c
     2 static inline void
     3 timer_execute(struct timer *T) {
     4     int idx = T->time & TIME_NEAR_MASK;
     5         
     6     while (T->near[idx].head.next) {
     7         struct timer_node *current = link_clear(&T->near[idx]);
     8         SPIN_UNLOCK(T);
     9         // dispatch_list don't need lock T
    10         dispatch_list(current);
    11         SPIN_LOCK(T);
    12     }
    13 }
    14 
    15 static inline void
    16 dispatch_list(struct timer_node *current) {
    17     do {
    18         struct timer_event * event = (struct timer_event *)(current+1);
    19         struct skynet_message message;
    20         message.source = 0;
    21         message.session = event->session;
    22         message.data = NULL;
    23         message.sz = (size_t)PTYPE_RESPONSE << MESSAGE_TYPE_SHIFT;
    24 
    25        skynet_context_push(event->handle, &message);
    26                
    27        struct timer_node * temp = current;
    28        current=current->next;
    29        skynet_free(temp);      
    30     } while (current);
    31 }

    每帧除了触发定时器外,还需重新分配定时器所在区间(timer_shift),因为T->near里保存即将触发的定时器,所以每TIME_NEAR-1(2^8-1)个滴答数才有可能需要分配(第22行)。否则,分配T->t中某个等级即可。

    当T->time的低8位不全为0时,不需要分配,所以每2^8个滴答数才有需要分配一次;

    当T->time的第9-14位不全为0时,重新分配T[0]等级,每2^8个滴答数分配一次,idx从1开始,每次分配+1;

    当T->time的第15-20位不全为0时,重新分配T[1]等级,每2^(8+6)个滴答数分配一次,idx从1开始,每次分配+1;

    当T->time的第21-26位不全为0时,重新分配T[2]等级,每2^(8+6+6)个滴答数分配一次,idx从1开始,每次分配+1;

    当T->time的第27-32位不全为0时,重新分配T[3]等级,每2^(8+6+6+6)个滴答数分配一次,idx从1开始,每次分配+1;

    即等级越大的定时器越遥远,越不关注,需要重新分配的次数也就越少。

     1 // skynet-src/skynet_timer.c
     2 static void
     3 move_list(struct timer *T, int level, int idx) {
     4     struct timer_node *current = link_clear(&T->t[level][idx]);
     5     while (current) {
     6         struct timer_node *temp=current->next;
     7         add_node(T,current);
     8         current=temp;
     9     }
    10 }
    11 
    12 static void
    13 timer_shift(struct timer *T) {
    14     int mask = TIME_NEAR;
    15     uint32_t ct = ++T->time;
    16     if (ct == 0) {
    17         move_list(T, 3, 0);
    18     } else {
    19         uint32_t time = ct >> TIME_NEAR_SHIFT;
    20         int i=0;
    21 
    22         while ((ct & (mask-1))==0) {
    23             int idx=time & TIME_LEVEL_MASK;
    24             if (idx!=0) {
    25                 move_list(T, i, idx);
    26                 break;                          
    27             }
    28             mask <<= TIME_LEVEL_SHIFT;
    29             time >>= TIME_LEVEL_SHIFT;
    30             ++i;
    31         }
    32     }
    33 }

    3. 如何使用

    在C层通过skynet_timeout创建一个定时器

    在Lua层通过skynet.timeout创建一个定时器,比如,skynet.timeout(200, f),即经过200个滴答数(2秒钟)后触发回调函数f。

  • 相关阅读:
    052 01 Android 零基础入门 01 Java基础语法 05 Java流程控制之循环结构 14 Eclipse下程序调试——debug2 多断点调试程序
    051 01 Android 零基础入门 01 Java基础语法 05 Java流程控制之循环结构 13 Eclipse下程序调试——debug入门1
    050 01 Android 零基础入门 01 Java基础语法 05 Java流程控制之循环结构 12 continue语句
    049 01 Android 零基础入门 01 Java基础语法 05 Java流程控制之循环结构 11 break语句
    048 01 Android 零基础入门 01 Java基础语法 05 Java流程控制之循环结构 10 案例——阶乘的累加和
    047 01 Android 零基础入门 01 Java基础语法 05 Java流程控制之循环结构 09 嵌套while循环应用
    046 01 Android 零基础入门 01 Java基础语法 05 Java流程控制之循环结构 08 for循环的注意事项
    045 01 Android 零基础入门 01 Java基础语法 05 Java流程控制之循环结构 07 for循环应用及局部变量作用范围
    剑指OFFER----面试题04.二维数组中的查找
    剑指OFFER----面试题03. 数组中重复的数字
  • 原文地址:https://www.cnblogs.com/RainRill/p/8516430.html
Copyright © 2011-2022 走看看