skynet源碼分析之定時器skynet_timer.c

skynet自帶定時器功能skynet-src/skynet_timer.c,在skynet啓動時會建立一個線程專門跑定時器。每幀(0.0025秒/幀)調用skynet_updatetime()html

 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.htmlnode

在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)個鏈表,大大節省了內存。服務器

以後,在不一樣狀況下,分配不一樣等級的定時器,等級越高,表示越遙遠,須要從新分配的次數越少。session

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]中ui

 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數組裏每一項的鏈表中的全部節點的到期滴答數是相同的。spa

調用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。

相關文章
相關標籤/搜索