在實際項目中磨隘,一個常用的做法是新起一個線程,專門管理定時器蹋订,定時來源使用rtc率挣、select等比較精確的來源,定時器超時后向主要的work線程發(fā)消息即可露戒,或者使用timefd接口椒功。
定時器的實現(xiàn)原理
定時器的實現(xiàn)依賴的是CPU時鐘中斷,時鐘中斷的精度就決定定時器精度的極限智什。一個時鐘中斷源如何實現(xiàn)多個定時器呢动漾?對于內核,簡單來說就是用特定的數(shù)據(jù)結構管理眾多的定時器荠锭,在時鐘中斷處理中判斷哪些定時器超時旱眯,然后執(zhí)行超時處理動作。而用戶空間程序不直接感知CPU時鐘中斷,通過感知內核的信號删豺、IO事件共虑、調度,間接依賴時鐘中斷呀页。用軟件來實現(xiàn)動態(tài)定時器常用數(shù)據(jù)結構有:時間輪妈拌、最小堆和紅黑樹。下面就是一些知名的實現(xiàn):
Linux內核的 Hierarchy 時間輪算法
Asio C++ Library最小堆定時器實現(xiàn)
nginx 使用紅黑樹結構管理定時器事件
內核定時器時間輪算法
單層時間輪算法的原理比較簡單:用一個數(shù)組表示時間輪蓬蝶,每個時鐘周期尘分,時間輪 current 往后走一個格,并處理掛在這個格子的定時器鏈表丸氛,如果超時則進行超時動作處理培愁,然后刪除定時器,沒有則剩余輪數(shù)減一缓窜。原理如圖:
Linux 內核則采用的是 Hierarchy 時間輪算法定续,Hierarchy 時間輪將單一的 bucket 數(shù)組分成了幾個不同的數(shù)組,每個數(shù)組表示不同的時間精度禾锤,Linux 內核中用 jiffies 記錄時間香罐,jiffies記錄了系統(tǒng)啟動以來經(jīng)過了多少tick。
Hierarchy 時間輪的原理大致如下时肿,下面是一個時分秒的Hierarchy時間輪,不同于Linux內核的實現(xiàn)港粱,但原理類似螃成。對于時分秒三級時間輪,每個時間輪都維護一個cursor查坪,新建一個timer時寸宏,要掛在合適的格子,剩余輪數(shù)以及時間都要記錄偿曙,到期判斷超時并調整位置氮凝。原理圖大致如下:
定時器的使用方法
在Linux 用戶空間程序開發(fā)中,常用的定期器可以分為兩類:
執(zhí)行一次的單次定時器 single-short望忆;
循環(huán)執(zhí)行的周期定時器 Repeating Timer罩阵;
其中,Repeating Timer 可以通過在Single-Shot Timer 終止之后启摄,重新再注冊到定時器系統(tǒng)里來實現(xiàn)稿壁。當一個進程需要使用大量定時器時,同樣利用時間輪歉备、最小堆或紅黑樹等結構來管理定時器傅是。而時鐘周期來源則需要借助系統(tǒng)調用,最終還是從時鐘中斷。Linux用戶空間程序的定時器可用下面方法來實現(xiàn):
通過alarm()或setitimer()系統(tǒng)調用喧笔,非阻塞異步帽驯,配合SIGALRM信號處理;
通過select()或nanosleep()系統(tǒng)調用书闸,阻塞調用尼变,往往需要新建一個線程;
通過timefd()調用梗劫,基于文件描述符享甸,可以被用于 select/poll 的應用場景;
通過RTC機制, 利用系統(tǒng)硬件提供的Real Time Clock機制, 計時非常精確梳侨;
上面方法沒提sleep()蛉威,因為Linux中并沒有系統(tǒng)調用sleep(),sleep()是在庫函數(shù)中實現(xiàn)走哺,是通過調用alarm()來設定報警時間蚯嫌,調用sigsuspend()將進程掛起在信號SIGALARM上,而且sleep()也只能精確到秒級上丙躏,精度不行择示。當使用阻塞調用作為定時周期來源時,可以單獨啟一個線程用來管理所有定時器晒旅,當定時器超時的時候栅盲,向業(yè)務線程發(fā)送定時器消息即可。
一個基于時間輪的定時器簡單實現(xiàn)
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#define TIME_WHEEL_SIZE 8
typedef void (*func)(int data);
struct timer_node {
struct timer_node *next;
int rotation;
func proc;
int data;
};
struct timer_wheel {
struct timer_node *slot[TIME_WHEEL_SIZE];
int current;
};
struct timer_wheel timer = {{0}, 0};
void tick(int signo)
{
// 使用二級指針刪進行單鏈表的刪除
struct timer_node **cur = &timer.slot[timer.current];
while (*cur) {
struct timer_node *curr = *cur;
if (curr->rotation > 0) {
curr->rotation--;
cur = &curr->next;
} else {
curr->proc(curr->data);
*cur = curr->next;
free(curr);
}
}
timer.current = (timer.current + 1) % TIME_WHEEL_SIZE;
alarm(1);
}
void add_timer(int len, func action)
{
int pos = (len + timer.current) % TIME_WHEEL_SIZE;
struct timer_node *node = malloc(sizeof(struct timer_node));
// 插入到對應格子的鏈表頭部即可, O(1)復雜度
node->next = timer.slot[pos];
timer.slot[pos] = node;
node->rotation = len / TIME_WHEEL_SIZE;
node->data = 0;
node->proc = action;
}
// test case1: 1s循環(huán)定時器
int g_sec = 0;
void do_time1(int data)
{
printf("timer %s, %d\n", __FUNCTION__, g_sec++);
add_timer(1, do_time1);
}
// test case2: 2s單次定時器
void do_time2(int data)
{
printf("timer %s\n", __FUNCTION__);
}
// test case3: 9s循環(huán)定時器
void do_time9(int data)
{
printf("timer %s\n", __FUNCTION__);
add_timer(9, do_time9);
}
int main()
{
signal(SIGALRM, tick);
alarm(1); // 1s的周期心跳
// test
add_timer(1, do_time1);
add_timer(2, do_time2);
add_timer(9, do_time9);
while(1) pause();
return 0;
}