• 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。

  • 相关阅读:
    Spring Cloud Data Flow整合UAA之使用LDAP进行账号管理
    2020,分手快乐;2021,且行且歌
    Spring Cloud Data Flow整合UAA使用外置数据库和API接口
    Spring Cloud Data Flow整合Cloudfoundry UAA服务做权限控制
    Spring自定义转换类,让@Value更方便
    2020年11月CKA新题考试心得体会
    使用Go module和GoLand初始化一个Go项目
    Spring Cloud Gateway简单入门,强大的微服务网关
    vue百度地图实现自定义覆盖物
    vue 中安装使用jquery
  • 原文地址:https://www.cnblogs.com/RainRill/p/8516430.html
Copyright © 2020-2023  润新知