作者:CppExplore 網(wǎng)址:http://www.shnenglu.com/CppExplore/
一、 基礎(chǔ)知識
1、時(shí)間類型。Linux下常用的時(shí)間類型有4個(gè):time_t,struct timeval,struct timespec,struct tm。
(1)time_t是一個(gè)長整型,一般用來表示用1970年以來的秒數(shù)。
(2)Struct timeval有兩個(gè)成員,一個(gè)是秒,一個(gè)是微妙。

struct timeval
{

long tv_sec; /**//* seconds */

long tv_usec; /**//* microseconds */
};
(3)struct timespec有兩個(gè)成員,一個(gè)是秒,一個(gè)是納秒。

struct timespec
{

time_t tv_sec; /**//* seconds */

long tv_nsec; /**//* nanoseconds */
};
(4)struct tm是直觀意義上的時(shí)間表示方法:

struct tm
{

int tm_sec; /**//* seconds */

int tm_min; /**//* minutes */

int tm_hour; /**//* hours */

int tm_mday; /**//* day of the month */

int tm_mon; /**//* month */

int tm_year; /**//* year */

int tm_wday; /**//* day of the week */

int tm_yday; /**//* day in the year */

int tm_isdst; /**//* daylight saving time */
};
2、 時(shí)間操作
(1) 時(shí)間格式間的轉(zhuǎn)換函數(shù)
主要是 time_t、struct tm、時(shí)間的字符串格式之間的轉(zhuǎn)換。看下面的函數(shù)參數(shù)類型以及返回值類型:
char *asctime(const struct tm *tm);
char *ctime(const time_t *timep);
struct tm *gmtime(const time_t *timep);
struct tm *localtime(const time_t *timep);
time_t mktime(struct tm *tm);
gmtime和localtime的參數(shù)以及返回值類型相同,區(qū)別是前者返回的格林威治標(biāo)準(zhǔn)時(shí)間,后者是當(dāng)?shù)貢r(shí)間。
(2) 獲取時(shí)間函數(shù)
兩個(gè)函數(shù),獲取的時(shí)間類型看原型就知道了:
time_t time(time_t *t);
int gettimeofday(struct timeval *tv, struct timezone *tz);
前者獲取time_t類型,后者獲取struct timeval類型,因?yàn)轭愋偷木壒剩罢咧荒芫_到秒,后者可以精確到微秒。
二、 延遲函數(shù)
主要的延遲函數(shù)有:sleep(),usleep(),nanosleep(),select(),pselect().
unsigned int sleep(unsigned int seconds);
void usleep(unsigned long usec);
int nanosleep(const struct timespec *req, struct timespec *rem);
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,struct timeval *timeout);
int pselect(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask);
alarm函數(shù)是信號方式的延遲,這種方式不直觀,這里不說了。
僅通過函數(shù)原型中時(shí)間參數(shù)類型,可以猜測sleep可以精確到秒級,usleep/select可以精確到微妙級,nanosleep和pselect可以精確到納秒級。
而實(shí)際實(shí)現(xiàn)中,linux上的nanosleep和alarm相同,都是基于內(nèi)核時(shí)鐘機(jī)制實(shí)現(xiàn),受linux內(nèi)核時(shí)鐘實(shí)現(xiàn)的影響,并不能達(dá)到納秒級的精度,man nanosleep也可以看到這個(gè)說明,man里給出的精度是:Linux/i386上是10 ms ,Linux/Alpha上是1ms。
這里有一篇文章http://blog.csdn.net/zhoujunyi/archive/2007/03/30/1546330.aspx,測試了不同延遲函數(shù)之間的精確度。文章給出的結(jié)論是linux上精度最高的是select,10ms級別。我在本機(jī)器測試select和pselect相當(dāng),都達(dá)到了1ms級的精度,精度高于文章中給出的10ms,sleep在秒級以上和usleep/nanosleep相當(dāng)。下面貼下我機(jī)器上1ms時(shí)候的測試結(jié)果,其他不貼了:
sleep 1000 0 -1000
usleep 1000 2974 1974
nanosleep 1000 2990 1990
select 1000 991 -9
pselect 1000 990 -10
gettimeofday 1000 1000 0
而使用gettimeofday循環(huán)不停檢測時(shí)間,可精確微秒級,不過不適宜用來做定時(shí)器模塊。
因此后面的定時(shí)期模塊將選擇select為延遲函數(shù)。
三、 定時(shí)器模塊需求以及實(shí)現(xiàn)概述
1、需求。從實(shí)現(xiàn)結(jié)果的角度說來,需求就是最終的使用方式。呵呵,不詳細(xì)描述需求了,先直接給出我實(shí)現(xiàn)的CTimer類的三個(gè)主要接口:

Class CTimer
{
Public:
CTimer(unsigned int vinterval,void (*vfunc)(CTimer *,void *),void *vdata,TimerType vtype);
void start();
void stop();
void reset(unsigned int vinterval);
};
使用定時(shí)器模塊的步驟如下:
(1) 實(shí)例化一個(gè)CTimer,參數(shù)的含義依次是:vinterval間隔時(shí)間(單位ms),vfunc是時(shí)間到回調(diào)的函數(shù),vdata回調(diào)函數(shù)使用的參數(shù),vtype定時(shí)器的類型,分一次型和循環(huán)型兩種。
(2) 調(diào)用start方法。
(3) 必要的時(shí)候調(diào)用stop和reset。
2、實(shí)現(xiàn)。簡單描述下定時(shí)器模塊的實(shí)現(xiàn),有一個(gè)manager單例類保存所有CTimer對象,開啟一線程運(yùn)行延遲函數(shù),每次延遲間隔到,掃描保存CTimer的容器,對每個(gè)CTimer對象執(zhí)行減少時(shí)間操作,減少到0則執(zhí)行回調(diào)函數(shù)。對一次性CTimer,超時(shí)則從容器中刪除,循環(huán)型的將間隔時(shí)間重置,不從容器中移除。
CTimer的start執(zhí)行將對象插入到manager容器中操作;stop執(zhí)行將對象從manager容器中刪除的操作;reset執(zhí)行先刪除,重置間隔,然后再放到容器中,reset不改變CTimer的定時(shí)器類型屬性。
四、 定時(shí)器模塊的數(shù)據(jù)結(jié)構(gòu)選擇
Manager類的容器要頻繁進(jìn)行的操作涉及插入、刪除、查詢等。
誤區(qū):(1)簡單看,好象該容器要是有序的,方便插入刪除等,貌似紅黑樹比較合適。其實(shí)不然,插入刪除操作的頻率很低,最頻繁的還是每次時(shí)延到,對容器的掃描并做時(shí)間減少操作,紅黑樹在做順序掃描相對鏈表并沒什么優(yōu)勢。
(2) 插入的時(shí)候依照順序鏈表的方式插入到合適的位置保持排序,以保證超時(shí)的對象都在鏈表的頭端。其實(shí)這也是沒必要的,每次時(shí)延到,對每一個(gè)對象都要做時(shí)間減少操作,因此不管是有序還是無序,都是一次掃描就執(zhí)行完下面操作:減少時(shí)間、判斷是否超時(shí),是則執(zhí)行回調(diào),繼續(xù)判斷是什么類型,一次型的則執(zhí)行完就移除,循環(huán)型則執(zhí)行完直接重置間隔就可。
因此,只需要能快速插入頭、刪除結(jié)點(diǎn)、遍歷就好。我的實(shí)現(xiàn)直接使用BSD內(nèi)核中的數(shù)據(jù)結(jié)構(gòu)LIST,插入頭、刪除時(shí)間復(fù)雜度都是1,遍歷就不說了。linux下/usr/include/sys下有頭文件queue.h里也有LIST結(jié)構(gòu)以及操作的定義。貌似linux下的少了遍歷宏:
#define LIST_FOREACH(var, head, field) \
for((var) = LIST_FIRST(head); \
(var)!= LIST_END(head); \
(var) = LIST_NEXT(var, field))
五、 詳細(xì)實(shí)現(xiàn)
這里帖出主要的代碼,請重點(diǎn)關(guān)注CTimerManager:: process方法,不再詳細(xì)說了。需要詳細(xì)的全部代碼,可來信索取,整體代碼很簡單,就兩個(gè)類。
class CTimer


{
friend class CTimerManager;
public:
typedef enum

{
TIMER_IDLE=0, //start前以及手動調(diào)用stop后的狀態(tài)
TIMER_ALIVE, //在manager的list里時(shí)候的狀態(tài)
TIMER_TIMEOUT //超時(shí)后被移除的狀態(tài),循環(huán)型的沒有
}TimerState;
typedef enum

{
TIMER_ONCE=0, //一次型
TIMER_CIRCLE //循環(huán)型
}TimerType;
CTimer(unsigned int vinterval,void (*vfunc)(CTimer *,void *),void *vdata,TimerType vtype);
void start();
void stop();
void reset(unsigned int vinterval);
~CTimer();
private:
unsigned int id_; //測試用
unsigned int m_interval; //間隔,不變
unsigned int m_counter; //開始設(shè)置為interval,隨延遲時(shí)間到,減少
TimerState m_state; //狀態(tài)
TimerType m_type; //類型
void (*m_func)(CTimer *,void *);//回調(diào)函數(shù)
void * m_data; //回調(diào)函數(shù)參數(shù)
LIST_ENTRY(CTimer) entry_; //LIST的使用方式
};

/**//*構(gòu)造函數(shù)*/
CTimer::CTimer(unsigned int vinterval,void (*vfunc)(CTimer *,void *),void *vdata,TimerType vtype):

m_interval(vinterval),m_counter(vinterval),
m_state(TIMER_IDLE),m_type(vtype),
m_func(vfunc),m_data(vdata)


{}

/**//*開始定時(shí)器*/
void CTimer::start()


{
CTimerManager::instance()->add_timer(this);
}

/**//*停止定時(shí)器*/
void CTimer::stop()


{
CTimerManager::instance()->remove_timer(this);
}

/**//*reset定時(shí)器*/
void CTimer::reset(unsigned int vinterval)


{
CTimerManager::instance()->remove_timer(this);
m_counter=m_interval=vinterval;
CTimerManager::instance()->add_timer(this);
}

/**//*析構(gòu)函數(shù),stop操作不能省略,避免delete前忘記stop*/
CTimer::~CTimer()


{
if(m_state==TIMER_ALIVE)
stop();
}
CTimerManager的:
class CTimerManager


{
public:
typedef enum

{
TIMER_MANAGER_STOP=0,
TIMER_MANAGER_START
}TimerManagerState;
static CTimerManager * instance();
void add_timer(CTimer * vtimer);//線程安全的add
void remove_timer(CTimer * vtimer);//線程安全的remove
void start(); //開始process線程
void stop(); //停止process線程
void dump();
protected:
static void * process(void *); //實(shí)際的定時(shí)器時(shí)間延遲線程
private:
void add_timer_(CTimer * vtimer);//非線程安全的add
void remove_timer_(CTimer * vtimer);//非線程安全的remove
CTimerManager();
static pthread_mutex_t m_mutex;
static CTimerManager * m_instance;
TimerManagerState m_state;
LIST_HEAD(,CTimer) list_; //LIST使用方式

static unsigned int mark; //測試,配合dump
};

/**//*singlton的double-check實(shí)現(xiàn)*/
CTimerManager * CTimerManager::instance()


{
if(m_instance==NULL)

{
pthread_mutex_lock(&m_mutex);
if(m_instance==NULL)

{
m_instance=new CTimerManager();
}
pthread_mutex_unlock(&m_mutex);
}
return m_instance;
}

/**//*process必須static,不能操作非static屬性,因此傳遞this指針*/
void CTimerManager:: start()


{
if(m_state==TIMER_MANAGER_STOP){
m_state=TIMER_MANAGER_START;
pthread_t pid;
pthread_create(&pid,0,process,this);
}
}

/**//*定時(shí)器模塊延遲時(shí)間線程*/
void * CTimerManager:: process(void * arg)


{
pthread_detach(pthread_self());

CTimerManager *manage=(CTimerManager *)arg;

CTimer *item;
struct timeval start,end;
unsigned int delay;
struct timeval tm;
gettimeofday(&end,0);

/**//*使用狀態(tài)控制線程運(yùn)行,進(jìn)而容易實(shí)現(xiàn)stop,也可以使用pthread_cancel粗暴的停止,需要考慮暫停點(diǎn)等問題*/
while(manage->m_state==TIMER_MANAGER_START)

{
tm.tv_sec=0;
tm.tv_usec=DEFULT_INTERVAL*1000;
start.tv_sec=end.tv_sec;
start.tv_usec=end.tv_usec;

/**//*不同系統(tǒng)的延遲函數(shù)精度不同,如果需要替換為其他延遲函數(shù),這附近修改下就好*/
while(select(0,0,0,0,&tm)<0&&errno==EINTR);
gettimeofday(&end,0);

delay=(end.tv_sec-start.tv_sec)*1000+(end.tv_usec-start.tv_usec)/1000;
pthread_mutex_lock(&manage->m_mutex);

LIST_FOREACH(item, &(manage->list_), entry_)

{
item->m_counter<delay?item->m_counter=0:item->m_counter-=delay;
if(item->m_counter==0)

{
if(item->m_func)
item->m_func(item,item->m_data);

if(item->m_type==CTimer::TIMER_ONCE)

{

/**//*一次型的,超時(shí),移除,并狀態(tài)CTimer::TIMER_TIMEOUT*/
manage->remove_timer_(item);
item->m_state=CTimer::TIMER_TIMEOUT;
}
else if(item->m_type==CTimer::TIMER_CIRCLE)

{

/**//*循環(huán)型的,重置counter就好*/
item->m_counter=item->m_interval;
}
}
}

pthread_mutex_unlock(&manage->m_mutex);
}
}
六、 討論
(1)精度問題。精度高,實(shí)時(shí)性高,但要求select等待的時(shí)間縮短,進(jìn)而增加對LIST結(jié)構(gòu)的掃描操作。精度低,實(shí)時(shí)性差,但會增加定時(shí)器線程的睡眠時(shí)間,減少對cpu的占用。一般的應(yīng)用系統(tǒng),應(yīng)該盡量降低精度,避免不必要的掃描,對具體系統(tǒng)可考察所用到的所有定時(shí)器的實(shí)際間隔,在允許的情況下,盡量降低精度,可通過修改代碼中的宏實(shí)現(xiàn)。為了降低定時(shí)器線程對cpu的占有時(shí)間,甚有更為粗獷型的定時(shí)器模塊實(shí)現(xiàn)為將延遲時(shí)間取list中最小的那個(gè)間隔,保證每次延遲時(shí)間到都有回調(diào)。
(2)加鎖區(qū)域問題。本文中的定時(shí)器模塊實(shí)現(xiàn),將定時(shí)器對象的時(shí)間減少以及函數(shù)回調(diào)的執(zhí)行等再同一個(gè)臨界區(qū)內(nèi)執(zhí)行,而有的定時(shí)器模塊實(shí)現(xiàn)是在加鎖區(qū)域執(zhí)行“時(shí)間減少”操作,將減少到0的對象放到另一個(gè)超時(shí)鏈表中,解鎖后再單獨(dú)掃描超時(shí)鏈表執(zhí)行回調(diào)操作。很明顯,后者縮短了加鎖時(shí)間,能及時(shí)響應(yīng)其他的線程的定時(shí)器對象的start以及stop操作。但是后者對定時(shí)器操作的時(shí)序性有誤差,直觀反應(yīng)就是可能在定時(shí)器執(zhí)行了stop操作以后,仍然會有超時(shí)回調(diào)發(fā)生,特別是回調(diào)參數(shù)是指針的情況,可能引起難以發(fā)現(xiàn)的bug,增加調(diào)試?yán)щy。在衡量兩者的利弊后,本文采用延長加鎖時(shí)間以保證操作的時(shí)序性。因此,在實(shí)際的使用,回調(diào)函數(shù)應(yīng)盡快返回,另一方面,盡量減少系統(tǒng)內(nèi)使用的定時(shí)器數(shù)目,這個(gè)主要原因是延遲時(shí)間到要掃描LIST,哪種方式都避免不了。
七、使用示例
#include "timer_manager.h"
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
void func(CTimer * timer, void *data)


{
printf("hi,%d\n",(int)(data));
}

/**//*隨便寫的,湊合著看吧。沒有CTimerManager::instance()->stop();也沒new對象。定時(shí)器對象可多次start和stop,使用上對暴露的接口沒有任何的契約式限制,可隨意調(diào)用*/
int main()


{
CTimerManager::instance()->start();
CTimer a(1000,func,(void *)1,CTimer::TIMER_CIRCLE);
CTimer a1(2000,func,(void *)11,CTimer::TIMER_ONCE);
CTimer a2(3000,func,(void *)12,CTimer::TIMER_ONCE);
CTimer a3(1000,func,(void *)13,CTimer::TIMER_ONCE);
a.start();
a1.start();
a2.start();
a3.start();
a.start();
a1.start();
a2.start();
a3.start();

sleep(1);
CTimerManager::instance()->dump();
sleep(1);
CTimerManager::instance()->dump();
a.reset(2000);
a1.stop();
a3.stop();

sleep(10);
return 0;
}
八、后記
昨晚寫好文章,不知為何無故丟失大半,郁悶。今早醒來,感覺還是有點(diǎn)地方要修改,:在start定時(shí)器線程的時(shí)候,傳入精度和誤差補(bǔ)償,一是根據(jù)實(shí)際需要調(diào)整精確度,二是彌補(bǔ)延遲函數(shù)的稍許誤差,以具有更好的伸縮性和精確度。本文旨在說明定時(shí)器模塊的內(nèi)部實(shí)現(xiàn)機(jī)制,詳細(xì)細(xì)節(jié)不再修改了。