muduo網路庫學習之EventLoop(一):事件迴圈類圖簡介和muduo 定時器TimeQueue

s1mba發表於2013-11-07

1、EventLoop、Channel、Poller 等類圖如下:

黑色菱形:組合;白色菱形:聚合;白色三角形:繼承;實線:關聯;

Channel是selectable IO channel,負責註冊與響應IO 事件,它不擁有file descriptor。
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)、定時函式,用於讓程式等待一段時間或安排計劃任務:
sleep
alarm
usleep
nanosleep
clock_nanosleep
getitimer / setitimer
timer_create / timer_settime / timer_gettime / timer_delete
timerfd_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);
// 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)

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

....省略every2


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/

相關文章