muduo_net程式碼剖析之定時器
1、Timer*類簡介
- 這裡涉及了3個類TimerId、Timer、TimerQueue,反映到實際使用,主要是EventLoop中的三個函式:runAt()、runAfter()、runEvery()。
- 簡單來說,TimerQueue是用來進行管理排程的; 而Timer是真正的超時事件(該Class中封裝了真正的超時回撥)
- 要知道Timer*怎麼與EventLoop結合起來,還是從一個示例開始:
通過runAt()、runAfter()、runEvery()函式與EventLoop結合起來
void printTid()
{
printf("pid = %d, tid = %d\n" , getpid(), muduo::CurrentThread::tid());
printf("now %s\n", muduo::Timestamp::now().toString().c_str());
}
void print(const char* msg)
{
printf("msg %s %s\n", muduo::Timestamp::now().toString().c_str(), msg);
if (++cnt == 20)
{
g_loop->quit();
}
}
int main()
{
printTid();
muduo: :EventLoop loop;
g_loop = &loop;
print("main");
loop.runAfter(1, boost::bind(print, "once1"));
#if 1
loop.runAfter(1.5, boost::bind(print, "once1.5"));
loop.runAfter(2.5, boost::bind(print, "once2.5"));
loop.runAfter(3.5, boost::bind(print, "once3.5"));
loop.runEvery(2, boost:: bind(print, "every2"));
loop.runEvery(3, boost::bind(print, "every3"));
#endif
loop.loop();
print("main loop exits");
sleep(1);
}
2、timerfd_* 和gettimeofday
2.1 使用timerfd_*系列函式來處理定時任務
timerfd是Linux為使用者程式提供的一個定時器介面。這個介面基於檔案描述符,通過檔案描述符的可讀事件進行超時通知,所以能夠被用於select/poll的應用場景。
(1) 先介紹兩個和時間相關的結構體,可以設定超時時間、超時的重複時間
struct itimerspec {
struct timespec it_interval; /* 之後的超時時間即每隔多長時間超時 */
struct timespec it_value; /* 定時器第一次超時時間 */
};
struct timespec {
time_t tv_sec; /* 秒*/
long tv_nsec; /* 納秒 */
};
(2) 再介紹timefd_*相關的3個函式
作用:建立一個定時器描述符timerfd
int timerfd_create(int clockid, int flags);
返回值:timerfd(檔案描述符)
引數:
clockid指定時間型別,有兩個值:
CLOCK_REALTIME :Systemwide realtime clock. 系統範圍內的實時時鐘
CLOCK_MONOTONIC:以固定的速率執行,從不進行調整和復位 ,它不受任何系統time-of-day時鐘修改的影響
flags:可以是0或者O_NONBLOCK/O_CLOEXEC(該fd在exec時不會被繼承下去)
作用:用來啟動或關閉有fd指定的定時器
int timerfd_settime(int fd, int flags,
const struct itimerspec *new_value, //超時時間
struct itimerspec *old_value //掩碼時間
);
引數:
fd:timerfd,有timerfd_create函式返回
fnew_value:指定新的超時時間,設定new_value.it_value非零則啟動定時器,否則關閉定時器;如果new_value.it_interval為0,則定時器只定時一次,即初始那次,否則之後每隔設定時間超時一次
old_value:不為null,則返回定時器這次設定之前的超時時間
flags:1代表設定的是絕對時間;為0代表相對時間。
作用:用於獲得定時器距離下次超時還剩下的時間
int timerfd_gettime(int fd, struct itimerspec *curr_value);
如果呼叫時定時器已經到期,並且該定時器處於迴圈模式(設定超時時間時struct itimerspec::it_interval不為0),那麼呼叫此函式之後定時器重新開始計時。
2.2 使用gettimeofday來獲取當前時間
這個函式被封裝在Timestamp類中,簡單示意如下:
class Timestamp
{
public:
Timestamp():microSecondsSinceEpoch_(0){}
Timestamp(uint_64 micro):microSecondsSinceEpoch_(mirco){}
static Timestamp now(){
struct timeval tv;
gettimeofday(&tv, NULL); //獲取當前時間
int64_t seconds = tv.tv_sec;
//該構造初始化了microSecsSinceEpoch_
return Timestamp(seconds * kMicroSecondsPerSecond + tv.tv_usec);
}
};
通過把時間進行微秒級別的量化從而方便對時間戳的比較。
3、Timer* 類的設計與實現
首先,是EventLoop類物件的初始化:可以看到,在建立EventLoop的時刻,將會為EventLoop物件建立一個定時器物件TimerQueue
EventLoop::EventLoop()
: looping_(false),
quit_(false),
threadId_(CurrentThread::tid()),
poller_(new Poller(this)),
timerQueue_(new TimerQueue(this))
{
//...
}
然後,看TimerQueue的建構函式:因為TimerQueue類中有loop_成員,因此可以將TimerQueue物件新增到loop_中並進行監聽定時事件的到來
TimerQueue::TimerQueue(EventLoop* loop)
: loop_(loop),
timerfd_(createTimerfd()),//呼叫::timerfd_create建立了timefd描述符
timerfdChannel_(loop, timerfd_),//使用timerfd_構造channel物件,並安插到loop上
timers_()//儲存Timer的關鍵結構
{
timerfdChannel_.setReadCallback( //設定回撥函式handleRead
boost::bind(&TimerQueue::handleRead, this));
timerfdChannel_.enableReading(); //監聽讀事件
}
當timerfd_上的讀事件到來時,將會觸發回撥函式TimerQueue::handleRead
void TimerQueue::handleRead()
{
loop_->assertInLoopThread();
Timestamp now(Timestamp::now());
readTimerfd(timerfd_, now);
std::vector<Entry> expired = getExpired(now);//獲取超時的事件
//依次呼叫Timer中的回撥。
for (std::vector<Entry>::iterator it = expired.begin();
it != expired.end(); ++it)
{
it->second->run(); //呼叫回撥函式
}
//執行完一次超時回撥後,這個時候需要重新設定定時器,把當前未過期的最早時間作為定時器最新時間
reset(expired, now);
}
建構函式幫我們做了很多事情,不過上述情況是假設TimerQueue中已經有一系列已經排好序列的時間事件了,現在要來看看怎麼新增定時器(即新增定時器的策略)
對應EventLoop中的函式就是runAt、runAfter、runEvery
其實主要是TimeQueue::addTimer函式,因為runAfter和runEvery都是通過設定不同的引數去呼叫TimeQueue::addTimer。
TimerId TimerQueue::addTimer(const TimerCallback& cb,//超時回撥
Timestamp when,//超時時間
double interval)//重複時間
{
Timer* timer = new Timer(cb, when, interval);//構造Timer
loop_->assertInLoopThread();
//把當前未過期的最早時間設定為定時器的超時時間
bool earliestChanged = insert(timer);
if (earliestChanged)
{
resetTimerfd(timerfd_, timer->expiration());
}
return TimerId(timer);
}
綜上所述,整個過程如下:
4、 Timer*類的原始碼分析
TimerId類
TimerId非常簡單,它被設計用來取消Timer的,它的結構很簡單,只有一個Timer指標和其序列號
class TimerId : public muduo::copyable
{
public:
TimerId()
: timer_(NULL),
sequence_(0)
{
}
TimerId(Timer* timer, int64_t seq)
: timer_(timer),
sequence_(seq)
{
}
friend class TimerQueue; //TimerQueue是TimerId的友元類,可以訪問TimerId的私有函式
private:
Timer* timer_; //Timer指標
int64_t sequence_; //序列號
};
Timer類
- 超時時間/重複時間間隔、定時器是否重複、定時器序列號、超時回撥函式
- run()呼叫回撥函式、restart用來重啟定時器(如果repeat_)
class Timer : noncopyable
{
public:
Timer(TimerCallback cb, Timestamp when, double interval)
: callback_(std::move(cb)), //回撥函式
expiration_(when), //一次的超時時刻
interval_(interval), //如果重複,間隔時間(超時時間間隔,如果是一次性定時器,該值為0)
repeat_(interval > 0.0), //是否重複
sequence_(s_numCreated_.incrementAndGet())//當前定時器的序列號
{ }
void run() const //超時時,呼叫回撥函式
{
callback_();
}
Timestamp expiration() const { return expiration_; } //返回超時時間
bool repeat() const { return repeat_; } //返回是否重複設定
int64_t sequence() const { return sequence_; } //返回序列號
void restart(Timestamp now) //重新設定超時時間
{
if (repeat_)//如果設定重複,則重新新增
{
expiration_ = addTime(now, interval_);//將now和interval_相加,重新設定超時時間
}
else //不重複設定
{
expiration_ = Timestamp::invalid(); //設為無效時間
}
}
static int64_t numCreated() { return s_numCreated_.get(); }
private:
const TimerCallback callback_; //超時回撥函式
Timestamp expiration_; //下一次的超時時刻
const double interval_; //超時時間間隔,如果是一次性定時器,該值為0
const bool repeat_; //是否重複
const int64_t sequence_; //定時器序號
static AtomicInt64 s_numCreated_;//定時器計數,當前已經建立的定時器數量
};
TimerQueue類
雖然TimerQueue中有Queue,但是其實現時基於Set的,而不是Queue。這樣可以高效地插入、刪除定時器,且找到當前已經超時的定時器。TimerQueue的public介面只有兩個,新增和刪除。
- TimerQueue的封裝是為了讓未到期的時間Timer有序的排列起來,這樣,能夠根據當前時間找到已經到期的Timer也能高效的新增和刪除Timer。
- 到期的時間應該被清除去執行相應的回撥,未到期的時間則應該有序的排列起來
總結定時器的使用方法
- 使用muduo庫中封裝好的定時器
EventLoop中使用runAt()、runAfter()、runEvery()新增定時器 - 手動建立定時器,詳細過程見下
(1) timerfd_create建立事件timerfd
(2) 用timerfd構造Channel物件
(3) Channel設定回撥函式、啟用讀事件
(4) timerfd_settime為timerfd設定超時時間
(5) loop.loop()進行監聽超時時間