首先,我们先要明白为什么需要设计这样一个定时器类?
在开发Linux网络程序时,通常需要维护多个定时器,如维护客户端心跳时间、检查多个数据包的超时重传等。如果采用Linux的SIGALARM信号实现,则会带来较大的系统开销,且不便于管理。
Muduo 的 TimerQueue 采用了最简单的实现(链表)来管理定时器,它的效率比不上常见的 binary heap 的做法,如果程序中大量(10 个以上)使用重复触发的定时器,或许值得考虑改用更高级的实现。由于目前还没有在一个程序里用过这么多定时器,暂时也不需要优化 TimerQueue。
(一)定时函数的选取
定时函数,用于让程序等待一段时间或安排计划任务: sleep alarm usleep nanosleep clock_nanosleep getitimer / setitimer timer_create / timer_settime / timer_gettime / timer_delete timerfd_create / timerfd_gettime / timerfd_settime
最终我们选择了下面的函数:
#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)
timerfd_* 入选的原因:
(1)sleep / alarm / usleep 在实现时有可能用了信号 SIGALRM,在多线程程序中处理信号是个相当麻烦的事情,应当尽量避免。(5)timerfd_create 把时间变成了一个文件描述符,该“文件”在定时器超时的那一刻变得可读,这样就能很方便地融入到 select/poll 框架中,用统一的方式来处理 IO 事件和超时事件,这也正是 Reactor 模式的长处。
传统的Reactor 利用select/poll/epoll 的timeout 来实现定时功能,但poll 和epoll 的定时精度只有毫秒,远低于timerfd_settime 的定时精度。
(二)TimerId、Timer、TimerQueue分析
muduo的定时器由三个类实现,TimerId、Timer、TimerQueue,用户只能看到第一个类,其它两个都是内部实现细节
TimerQueue的接口很简单,只有两个函数addTimer和cancel。
EventLoop
runAt 在某个时刻运行定时器
runAfter 过一段时间运行定时器
runEvery 每隔一段时间运行定时器
cancel 取消定时器
TimerQueue数据结构的选择,能快速根据当前时间找到已到期的定时器,也要高效的添加和删除Timer,因而可以用二叉搜索树,用map或者set
typedef std::pair<Timestamp, Timer*> Entry;
typedef std::set<Entry> TimerList;
#ifndef MUDUO_NET_TIMERID_H #define MUDUO_NET_TIMERID_H #include <muduo/base/copyable.h> namespace muduo { namespace net { class Timer; /// An opaque identifier, for canceling Timer. class TimerId : public muduo::copyable { public: TimerId() : timer_(NULL), sequence_(0) { } TimerId(Timer* timer, int64_t seq) : timer_(timer), sequence_(seq) { } // default copy-ctor, dtor and assignment are okay friend class TimerQueue; private: Timer* timer_; int64_t sequence_; }; } } #endif // MUDUO_NET_TIMERID_H(2)Timer 是对定时操作的高度抽象,有多个数据成员,可以根据不同情况设置每个Timer超时的回调函数。
/// Internal class for timer event. /// class Timer : boost::noncopyable { public: Timer(const TimerCallback& cb, Timestamp when, double interval) : callback_(cb), expiration_(when), interval_(interval), 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); 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_; // 定时器计数,当前已经创建的定时器数量 }; } }(3)TimerQueue的公有接口很简单,只有两个函数addTimer和cancel, TimerQueue 数据结构的选择,能快速根据当前时间找到已到期的定时器,也要高效的添加和删除Timer,因而可以用二叉搜索树,用map或者set。Timequeue关注最早的定时器,getExpired返回所有的超时定时器列表,使用low_bound返回第一个值>=超时定时器的迭代器。
class TimerQueue : boost::noncopyable { public: TimerQueue(EventLoop* loop); ~TimerQueue(); /// /// 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: // FIXME: use unique_ptr<Timer> instead of raw pointers. // unique_ptr是C++ 11标准的一个独享所有权的智能指针 // 无法得到指向同一对象的两个unique_ptr指针 // 但可以进行移动构造与移动赋值操作,即所有权可以移动到另一个对象(而非拷贝构造) typedef std::pair<Timestamp, Timer*> Entry; typedef std::set<Entry> TimerList; typedef std::pair<Timer*, int64_t> ActiveTimer; typedef std::set<ActiveTimer> ActiveTimerSet; // 以下成员函数只可能在其所属的I/O线程中调用,因而不必加锁。 // 服务器性能杀手之一是锁竞争,所以要尽可能少用锁 void addTimerInLoop(Timer* timer); void cancelInLoop(TimerId timerId); // called when timerfd alarms void handleRead(); // move out all expired timers // 返回超时的定时器列表 std::vector<Entry> getExpired(Timestamp now); void reset(const std::vector<Entry>& expired, Timestamp now); bool insert(Timer* timer); EventLoop* loop_; // 所属EventLoop const int timerfd_; Channel timerfdChannel_; // Timer list sorted by expiration TimerList timers_; // timers_是按到期时间排序 // for cancel() // timers_与activeTimers_保存的是相同的数据 // timers_是按到期时间排序,activeTimers_是按对象地址排序 ActiveTimerSet activeTimers_; bool callingExpiredTimers_; /* atomic */ ActiveTimerSet cancelingTimers_; // 保存的是被取消的定时器 };解释一个关键点:
struct Foo { Foo() { cout << "Foo ctor" << endl; } Foo(const Foo&) { cout << "Foo copy ctor" << endl; } void operator=(const Foo&) { cout << "Foo operator=" << endl; } ~Foo() { cout << "Foo dtor" << endl; } }; Foo make_foo() { Foo f; return f; }make_foo函数的返回对象,在linux下g++和vc++Release版本下,不会调用拷贝构造函数,而是直接返回对象,这就叫做rvo优化。但是vc下的debug版本没有做这个优化,读者可以自己测试一下。