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)個鏈表,大大節省了記憶體,
之后,在不同情況下,分配不同等級的定時器,等級越高,表示越遙遠,需要重新分配的次數越少,
對于從事游戲客戶端開發的朋友,界面開發很繁瑣,客戶端技術更新很快,很少能接觸底層開發,想轉型做游戲服務端開發的朋友,學習skynet可以了解服務器的整體架構流程,基礎組件以及服務端的編程思維,
對于從事小游戲開發,skynet是一個輕量級游戲服務器框架,同時學習服務端,能進行全堆疊開發
關于Skynet這里給大家推薦一個學習視頻,里面詳細講解了以下知識點內容,視頻鏈接:https://ke.qq.com/course/2806743?flowToken=1030833

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,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/243227.html
標籤:其他
上一篇:武漢有哪些優秀的互聯網公司?
下一篇:扭倒費賭局問題
