muduo網路庫學習之EventLoop(一):事件迴圈類圖簡介和muduo 定時器TimeQueue
1、EventLoop、Channel、Poller 等類圖如下:
黑色菱形:組合;白色菱形:聚合;白色三角形:繼承;實線:關聯;
Channel是selectable IO channel,負責註冊與響應IO 事件,它不擁有file descriptor。
Channel是Acceptor、Connector、EventLoop、TimerQueue、TcpConnection的成員。
Channel是Acceptor、Connector、EventLoop、TimerQueue、TcpConnection的成員。
一個EventLoop物件對應一個Poller成員物件,boost::scoped_ptr<Poller> poller_;
//Poller是個抽象類,具體可以是EPollPoller(預設) 或者PollPoller
Poller類裡面有三個純虛擬函式,需要子類實現:
C++ Code
1
2 3 4 5 6 7 8 9 10 11 |
/// Polls the I/O events.
/// Must be called in the loop thread. virtual Timestamp poll(int timeoutMs, ChannelList *activeChannels) = 0; /// Changes the interested I/O events. /// Must be called in the loop thread. virtual void updateChannel(Channel *channel) = 0; /// Remove the channel, when it destructs. /// Must be called in the loop thread. virtual void removeChannel(Channel *channel) = 0; |
對於PollPoller來說,一個fd對應一個struct pollfd(pollfd.fd),一個fd 對應一個channel*;這個fd 可以是socket, eventfd, timerfd, signalfd; 如下:
C++ Code
1
2 3 4 |
typedef std::vector<struct pollfd> PollFdList;
typedef std::map<int, Channel *> ChannelMap; // key是檔案描述符,value是Channel* PollFdList pollfds_; ChannelMap channels_; |
對於EPollPoller 來說,一個channel* 對應一個fd, 一個channel* 對應一個struct epoll_event(epoll_event.data.ptr)
C++ Code
1
2 3 4 |
typedef std::vector<struct epoll_event> EventList;
typedef std::map<int, Channel *> ChannelMap; EventList events_; ChannelMap channels_; |
一個執行緒最多隻能有一個EventLoop物件,這種執行緒被稱為IO執行緒。一個EventLoop物件對應多個Channel物件,但只有wakeupChannel_生存期由EventLoop控制, timerfdChannel_生存期由TimeQueue管理。
(boost::scoped_ptr<Channel> wakeupChannel_; // 納入poller_來管理 int wakeupFd_; // eventfd函式建立 )
其餘以Channel* 方式管理,如下:
C++ Code
1
2 |
typedef std::vector<Channel *> ChannelList;
ChannelList activeChannels_; // Poller返回的活動通道 |
下面是Channel 類簡化:
C++ Code
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
/// /// A selectable I/O channel. /// /// This class doesn't own the file descriptor. /// The file descriptor could be a socket, /// an eventfd, a timerfd, or a signalfd class Channel : boost::noncopyable { public: typedef boost::function<void()> EventCallback; typedef boost::function<void(Timestamp)> ReadEventCallback; Channel(EventLoop *loop, int fd); ~Channel(); void handleEvent(Timestamp receiveTime); void setReadCallback(const ReadEventCallback &cb) { readCallback_ = cb; } void setWriteCallback(const EventCallback &cb) { writeCallback_ = cb; } void setCloseCallback(const EventCallback &cb) { closeCallback_ = cb; } void setErrorCallback(const EventCallback &cb) { errorCallback_ = cb; } void enableReading() { events_ |= kReadEvent; update(); } ............
private: boost::weak_ptr<void> tie_; const int fd_; // 檔案描述符,但不負責關閉該檔案描述符 int events_; // 關注的事件 int revents_; // poll/epoll返回的事件 int index_; // used by PollPoller.表示在poll的事件陣列中的序號
// used by EPollPoller. 表示某channel的狀態(新建立,已關注,取消關注)
ReadEventCallback readCallback_;
EventCallback writeCallback_; EventCallback closeCallback_; EventCallback errorCallback_; }; |
|
C++ Code
1
2 3 4 5 6 7 8 9 10 |
#define POLLIN 0x0001
#define POLLPRI 0x0002 #define POLLOUT 0x0004 #define POLLERR 0x0008 #define POLLHUP 0x0010 #define POLLNVAL 0x0020 const int Channel::kNoneEvent = 0; const int Channel::kReadEvent = POLLIN | POLLPRI; const int Channel::kWriteEvent = POLLOUT; |
2、定時函式選擇 和 muduo 定時器
(1)、Linux 的計時函式,用於獲得當前時間:
time(2) / time_t (秒)ftime(3) / struct timeb (毫秒)gettimeofday(2) / struct timeval (微秒)clock_gettime(2) / struct timespec (納秒)gmtime / localtime / timegm / mktime / strftime / struct tm (這些與當前時間無關)
(2)、定時函式,用於讓程式等待一段時間或安排計劃任務:
sleepalarmusleepnanosleepclock_nanosleepgetitimer / setitimertimer_create / timer_settime / timer_gettime / timer_deletetimerfd_create / timerfd_gettime / timerfd_settime
取捨如下:
• (計時)只使用gettimeofday 來獲取當前時間。
• (定時)只使用timerfd_* 系列函式來處理定時。
gettimeofday 入選原因:(這也是muduo::Timestamp class 的主要設計考慮)
1. time 的精度太低,ftime 已被廢棄,clock_gettime 精度最高,但是它系統呼叫的開銷比gettimeofday 大。2. 在x86-64 平臺上,gettimeofday 不是系統呼叫,而是在使用者態實現的(搜vsyscall),沒有上下文切換和陷入核心的開銷。3. gettimeofday 的解析度(resolution) 是1 微秒,足以滿足日常計時的需要。muduo::Timestamp 用一個int64_t 來表示從Epoch 到現在的微秒數,其範圍可達上下30 萬年。
timerfd_* 入選的原因:
These system calls create and operate on a timer that delivers timer expiration notifications via a file descriptor.
#include <sys/timerfd.h>
int timerfd_create(int clockid, int flags);
int timerfd_create(int clockid, int flags);
// timerfd_create() creates a new timer object, and returns a file descriptor that refers to that timer.
int timerfd_settime(int fd, int flags, const struct itimerspec *new_value, struct itimerspec *old_value);
int timerfd_gettime(int fd, struct itimerspec *curr_value)
int timerfd_settime(int fd, int flags, const struct itimerspec *new_value, struct itimerspec *old_value);
int timerfd_gettime(int fd, struct itimerspec *curr_value)
sleep / alarm / usleep 在實現時有可能用了訊號 SIGALRM,在多執行緒程式中處理訊號是個相當麻煩的事情,應當儘量避免nanosleep 和 clock_nanosleep 是執行緒安全的,但是在非阻塞網路程式設計中,絕對不能用讓執行緒掛起的方式來等待一段時間,程式會失去響應。正確的做法是註冊一個時間回撥函式。getitimer 和 timer_create 也是用訊號來 deliver 超時,在多執行緒程式中也會有麻煩。timer_create 可以指定訊號的接收方是程式還是執行緒,算是一個進步,不過在訊號處理函式(signal handler)能做的事情實在很受限。timerfd_create 把時間變成了一個檔案描述符,該“檔案”在定時器超時的那一刻變得可讀,這樣就能很方便地融入到 select/poll 框架中,用統一的方式來處理 IO 事件和超時事件,這也正是 Reactor 模式的長處。傳統的Reactor 利用select/poll/epoll 的timeout 來實現定時功能,但poll 和epoll 的定時精度只有毫秒,遠低於timerfd_settime 的定時精度。
(3)、muduo的定時器由三個類實現,TimerId、Timer、TimerQueue,使用者只能看到第一個類,其它兩個都是內部實現細節
TimerId 只有兩個成員,TimerId主要用於取消Timer:
C++ Code
1
2 3 4 5 6 7 8 9 10 11 12 |
/// An opaque identifier, for canceling Timer.
/// class TimerId : public muduo::copyable { // default copy-ctor, dtor and assignment are okay friend class TimerQueue; private: Timer *timer_; int64_t sequence_; //時鐘序號 }; |
Timer 有多個資料成員,可以設定每個Timer超時的回撥函式
C++ Code
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
///
/// Internal class for timer event. /// class Timer : boost::noncopyable { public: void run() const { callback_(); } private: const TimerCallback callback_; // 定時器回撥函式 Timestamp expiration_; // 下一次的超時時刻 const double interval_; // 超時時間間隔,如果是一次性定時器,該值為0 const bool repeat_; // 是否重複 const int64_t sequence_; // 定時器序號 static AtomicInt64 s_numCreated_; // 定時器計數,當前已經建立的定時器數量 }; |
TimerQueue的公有介面很簡單,只有兩個函式addTimer和cancel, TimerQueue 資料結構的選擇,能快速根據當前時間找到已到期的定時器,也要高效的新增和刪除Timer,因而可以用二叉搜尋樹,用map或者set.
lower_bound(x); 返回第一個>=x 的元素的iterator位置;upper_bound(); 返回第一個>x的元素的iterator位置。
RVO優化:在linux g++ 會優化,VC++ 在release 模式下會優化,即函式返回物件時不會呼叫拷貝函式。
C++ Code
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
///
/// A best efforts timer queue. /// No guarantee that the callback will be on time. /// class TimerQueue : boost::noncopyable { public: /// /// Schedules the callback to be run at given time, /// repeats if @c interval > 0.0. /// /// Must be thread safe. Usually be called from other threads. // 一定是執行緒安全的,可以跨執行緒呼叫。通常情況下被其它執行緒呼叫。 TimerId addTimer(const TimerCallback &cb, Timestamp when, double interval); void cancel(TimerId timerId); private: typedef std::pair<Timestamp, Timer *> Entry; typedef std::set<Entry> TimerList; EventLoop *loop_; // 所屬EventLoop const int timerfd_; // timerfd_create 函式建立 Channel timerfdChannel_; // Timer list sorted by expiration TimerList timers_; // timers_是按到期時間排序 }; |
EventLoop類中的定時器操作函式:
C++ Code
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
TimerId EventLoop::runAt(const Timestamp &time, const TimerCallback &cb)
{ return timerQueue_->addTimer(cb, time, 0.0); } TimerId EventLoop::runAfter(double delay, const TimerCallback &cb) { Timestamp time(addTime(Timestamp::now(), delay)); return runAt(time, cb); } TimerId EventLoop::runEvery(double interval, const TimerCallback &cb) { Timestamp time(addTime(Timestamp::now(), interval)); return timerQueue_->addTimer(cb, time, interval); } void EventLoop::cancel(TimerId timerId) { return timerQueue_->cancel(timerId); } |
3、時序分析:
構造一個EventLoop物件,建構函式初始化列表,構造 timeQueue_ 成員 timerQueue_(new TimerQueue(this)),
呼叫TimeQueue 建構函式,函式內:
C++ Code
1
2 3 4 |
timerfdChannel_.setReadCallback(
boost::bind(&TimerQueue::handleRead, this)); // we are always reading the timerfd, we disarm it with timerfd_settime. timerfdChannel_.enableReading(); |
即註冊timerfdChannel_的回撥函式為TimerQueue::handleRead(), 並關注此channel 的可讀事件。
TimerQueue 中有多個定時器,一次性的和重複的,事件迴圈開始EventLoop::loop(),當最早到期定時器超時時,poll() 返回timerfd_ 的可讀事件(timerfdChannel_),呼叫Channel::handleEvent(),呼叫readCallback_(receiveTime); 進而呼叫Channel::setReadCallback 註冊的TimerQueue::handleRead(), 在函式內先read 掉timerfd_資料,避免一直觸發可讀事件,接著遍歷TimerQueue中此時所有超時的定時器,呼叫每個定時器構造時傳遞的回撥函式。
測試程式:
C++ Code
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
#include <muduo/net/EventLoop.h>
//#include <muduo/net/EventLoopThread.h> #include <muduo/base/Thread.h> #include <boost/bind.hpp> #include <stdio.h> #include <unistd.h> using namespace muduo; using namespace muduo::net; int cnt = 0; EventLoop *g_loop; void printTid() { printf("pid = %d, tid = %d\n", getpid(), CurrentThread::tid()); printf("now %s\n", Timestamp::now().toString().c_str()); } void print(const char *msg) { printf("msg %s %s\n", Timestamp::now().toString().c_str(), msg); if (++cnt == 20) { g_loop->quit(); } } void cancel(TimerId timer) { g_loop->cancel(timer); printf("cancelled at %s\n", Timestamp::now().toString().c_str()); } int main() { printTid(); sleep(1); { EventLoop loop; g_loop = &loop; print("main"); loop.runAfter(1, boost::bind(print, "once1")); 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")); TimerId t45 = loop.runAfter(4.5, boost::bind(print, "once4.5")); loop.runAfter(4.2, boost::bind(cancel, t45)); loop.runAfter(4.8, boost::bind(cancel, t45)); loop.runEvery(2, boost::bind(print, "every2")); TimerId t3 = loop.runEvery(3, boost::bind(print, "every3")); loop.runAfter(9.001, boost::bind(cancel, t3)); loop.loop(); print("main loop exits"); } } |
輸出比較多,刪除了一些重複的:
simba@ubuntu:~/Documents/build/debug/bin$ ./reactor_test04
20131107 13:46:35.850671Z 4042 TRACE IgnoreSigPipe Ignore SIGPIPE - EventLoop.cc:51
pid = 4042, tid = 4042
now 1383831995.852329
20131107 13:46:36.853813Z 4042 TRACE updateChannel fd = 4 events = 3 - EPollPoller.cc:104
20131107 13:46:36.854568Z 4042 TRACE EventLoop EventLoop created 0xBFB125F4 in thread 4042 - EventLoop.cc:76
20131107 13:46:36.855189Z 4042 TRACE updateChannel fd = 5 events = 3 - EPollPoller.cc:104
msg 1383831996.855730 main
20131107 13:46:36.856275Z 4042 TRACE loop EventLoop 0xBFB125F4 start looping - EventLoop.cc:108
20131107 13:46:37.856698Z 4042 TRACE poll 1 events happended - EPollPoller.cc:65
20131107 13:46:37.857372Z 4042 TRACE printActiveChannels {4: IN } - EventLoop.cc:271
20131107 13:46:37.858261Z 4042 TRACE readTimerfd TimerQueue::handleRead() 1 at 1383831997.858215 - TimerQueue.cc:62
msg 1383831997.858568 once1
20131107 13:46:38.356775Z 4042 TRACE poll 1 events happended - EPollPoller.cc:65
20131107 13:46:38.356855Z 4042 TRACE printActiveChannels {4: IN} - EventLoop.cc:271
20131107 13:46:38.356883Z 4042 TRACE readTimerfd TimerQueue::handleRead() 1 at 1383831998.356876 - TimerQueue.cc:62
msg 1383831998.356910 once1.5
msg 1383831998.856871 every2
msg 1383831999.356891 once2.5
msg 1383831999.856996 every3
msg 1383832000.356955 once3.5
msg 1383832000.857969 every2
cancelled at 1383832001.057005
cancelled at 1383832001.657036
msg 1383832002.858077 every3
msg 1383832002.858094 every2
msg 1383832004.859132 every2
cancelled at 1383832005.858189
msg 1383832005.858198 every3
msg 1383832006.860228 every2
msg 1383832008.861321 every2
20131107 13:46:35.850671Z 4042 TRACE IgnoreSigPipe Ignore SIGPIPE - EventLoop.cc:51
pid = 4042, tid = 4042
now 1383831995.852329
20131107 13:46:36.853813Z 4042 TRACE updateChannel fd = 4 events = 3 - EPollPoller.cc:104
20131107 13:46:36.854568Z 4042 TRACE EventLoop EventLoop created 0xBFB125F4 in thread 4042 - EventLoop.cc:76
20131107 13:46:36.855189Z 4042 TRACE updateChannel fd = 5 events = 3 - EPollPoller.cc:104
msg 1383831996.855730 main
20131107 13:46:36.856275Z 4042 TRACE loop EventLoop 0xBFB125F4 start looping - EventLoop.cc:108
20131107 13:46:37.856698Z 4042 TRACE poll 1 events happended - EPollPoller.cc:65
20131107 13:46:37.857372Z 4042 TRACE printActiveChannels {4: IN } - EventLoop.cc:271
20131107 13:46:37.858261Z 4042 TRACE readTimerfd TimerQueue::handleRead() 1 at 1383831997.858215 - TimerQueue.cc:62
msg 1383831997.858568 once1
20131107 13:46:38.356775Z 4042 TRACE poll 1 events happended - EPollPoller.cc:65
20131107 13:46:38.356855Z 4042 TRACE printActiveChannels {4: IN} - EventLoop.cc:271
20131107 13:46:38.356883Z 4042 TRACE readTimerfd TimerQueue::handleRead() 1 at 1383831998.356876 - TimerQueue.cc:62
msg 1383831998.356910 once1.5
msg 1383831998.856871 every2
msg 1383831999.356891 once2.5
msg 1383831999.856996 every3
msg 1383832000.356955 once3.5
msg 1383832000.857969 every2
cancelled at 1383832001.057005
cancelled at 1383832001.657036
msg 1383832002.858077 every3
msg 1383832002.858094 every2
msg 1383832004.859132 every2
cancelled at 1383832005.858189
msg 1383832005.858198 every3
msg 1383832006.860228 every2
msg 1383832008.861321 every2
....省略every2
msg 1383832020.867925 main loop exits
msg 1383832020.867925 main loop exits
程式中設定了多次定時器,0,1,2檔案描述符被標準輸入輸出佔據,epollfd_ = 3(epoll_create1 建立), timerfd_ = 4, wakeupFd_ = 5(見這裡), 可以看到每次定時時間到,timerfd_ 就會可讀,執行定時器回撥函式。4.5s的定時不會超時,因為還沒到時間的時候已經被取消了;
間隔3s的定時只超時3次,因為9s後被取消了;間隔2s的超時執行20次後g_loop->quit(),loop.loop()迴圈中判斷條件後退出事件迴圈。
參考:
《UNP》
muduo manual.pdf
《linux 多執行緒伺服器程式設計:使用muduo c++網路庫》
http://www.ibm.com/developerworks/cn/linux/l-cn-timers/相關文章
- muduo網路庫學習之EventLoop(四):EventLoopThread 類、EventLoopThreadPool 類OOPthread
- muduo網路庫學習之EventLoop(七):TcpClient、ConnectorOOPTCPclient
- muduo網路庫學習之muduo_http 庫涉及到的類HTTP
- muduo網路庫學習之muduo_inspect 庫涉及到的類
- muduo網路庫學習之EventLoop(二):程式(執行緒)wait/notify 和 EventLoop::runInLoopOOP執行緒AI
- muduo網路庫學習之EventLoop(六):TcpConnection::send()、shutdown()、handleRead()、handleWrite()OOPTCP
- muduo網路庫學習筆記(10):定時器的實現筆記定時器
- muduo網路庫Timestamp類
- muduo網路庫學習筆記(1):Timestamp類筆記
- muduo網路庫學習筆記(3):Thread類筆記thread
- muduo網路庫學習之EventLoop(五):TcpConnection生存期管理(連線關閉)OOPTCP
- muduo網路庫學習筆記(12):TcpServer和TcpConnection類筆記TCPServer
- muduo網路庫Exception異常類Exception
- muduo網路庫學習之EventLoop(三):Socket、Acceptor、TcpServer、TcpConnection(連線建立,接收訊息)OOPTCPServer
- muduo網路庫學習之ThreadLocal 類、ThreadLocalSingleton類封裝知識點thread封裝
- muduo網路庫學習之Timestamp類、AtomicIntegerT 類封裝中的知識點封裝
- muduo網路庫AtomicIntegerT原子整數類
- muduo網路庫學習筆記(2):原子性操作筆記
- muduo網路庫學習筆記(8):高效日誌類的封裝筆記封裝
- muduo網路庫學習之BlockinngQueue類、ThreadPool 類、Singleton類封裝中的知識點BloCthread封裝
- muduo網路庫學習之MutexLock類、MutexLockGuard類、Condition類、CountDownLatch類封裝中的知識點MutexCountDownLatch封裝
- muduo網路庫使用心得
- muduo網路庫學習筆記(14):chargen服務示例筆記
- muduo網路庫學習筆記(6):單例類(執行緒安全的)筆記單例執行緒
- muduo網路庫學習筆記(4):互斥量和條件變數筆記變數
- muduo網路庫學習之Logger類、LogStream類、LogFile類封裝中的知識點封裝
- muduo網路庫編譯安裝編譯
- muduo網路庫學習筆記(11):有用的runInLoop()函式筆記OOP函式
- muduo網路庫學習筆記(13):TcpConnection生命期的管理筆記TCP
- muduo網路庫學習筆記(7):執行緒特定資料筆記執行緒
- nodejs事件和事件迴圈簡介NodeJS事件
- JS事件迴圈EventLoop初探JS事件OOP
- JavaScript-事件迴圈-eventLoopJavaScript事件OOP
- muduo網路庫學習筆記(9):Reactor模式的關鍵結構筆記React模式
- muduo網路庫學習筆記(5):執行緒池的實現筆記執行緒
- js事件迴圈機制 EventLoop 【瀏覽器和node】JS事件OOP瀏覽器
- muduo網路庫學習之Exception類、Thread 類封裝中的知識點(重點講pthread_atfork())Exceptionthread封裝
- muduo網路庫學習筆記(15):關於使用stdio和iostream的討論筆記iOS