mini-muduo版本传送门
version 0.00 从epoll构建muduo-1 mini-muduo介绍
version 0.01 从epoll构建muduo-2 最简单的epoll
version 0.02 从epoll构建muduo-3 加入第一个类,顺便介绍reactor
version 0.03 从epoll构建muduo-4 加入Channel
version 0.04 从epoll构建muduo-5 加入Acceptor和TcpConnection
version 0.05 从epoll构建muduo-6 加入EventLoop和Epoll
version 0.06 从epoll构建muduo-7 加入IMuduoUser
version 0.07 从epoll构建muduo-8 加入发送缓冲区和接收缓冲区
version 0.08 从epoll构建muduo-9 加入onWriteComplate回调和Buffer
version 0.09 从epoll构建muduo-10 Timer定时器
version 0.11 从epoll构建muduo-11 单线程Reactor网络模型成型
version 0.12 从epoll构建muduo-12 多线程代码入场
version 0.13 从epoll构建muduo-13 Reactor + ThreadPool 成型
mini-muduo v0.09版本,实现了Timer定时器。mini-muduo完整可运行的示例可从github下载,使用命令git checkout v0.09可切换到此版本,在线浏览此版本到这里。
这个版本是一个里程碑版本,到这个版本为止,可以说一个单线程的Reactor模式网络库基本成型,程序里的类各司其职,协同完成网络数据的首发。后续版本只是在此基础上增加了多个工作线程(耗费CPU或需要等待磁盘读写的线程)和多个IO线程(IO multiplexing 的那个线程,也就是跑select/poll/epoll的线程)。
1 原书中对Timer的介绍分布在两个地方,<<7.8定时器>>和<<8.2 TimerQueue定时器>>,作者在7.82节总结了选择timerfd来作为多线程服务器程序的定时器的原因:
1 sleep(3)/alarm(2)/usleep(3)在实现时有可能用了SIGALRM信号, 在多线程程序中处理信号是个相当麻烦的事情,应当尽量避免
2 nanosleep(2)和clock_nanosleep(2)是线程安全的,但是在非阻塞网络编程中,绝对不能用让线程挂起的方式来等待一段时间,这样一来程序会失去响应。正确的做法是注册一个时间回调函数。
3 getitimer(2)和timer_create(2)也是用信号来deliver超时,在多线程程序中也会有麻烦。
4 timerfd_create(2)把时间变成了一个文件描述符,该文件描述符在定时器超时的那一刻变得刻度,这样就能很方便的融入select(2)/poll(2)框架中,用统一的方式来处理IO时间和超时事件。
5 传统的Reactor利用select(2)/poll(2)/epoll(4)/的timeout来实现定时功能,但poll(2)/和epoll_wait(2)的定时精度只有毫秒,远低于timerfd_settime(2)的定时精度。
2 有必要先看看最进本的timerfd是如何工作的,
注意下面粘贴代码/行数都是mini-muduo里的,而非muduo,当然了muduo的实现原理是一样的。
在我们的系统里,使用timerfd作为定时器,实际用到了下面5个函数,其中前两个为timer文件描述专用,后面三个可以用在多种文件描述符上。
3 看用户是如何使用Timer的,用户要使用网络库的定时器,必须通过EventLoop。EventLoop有三个接口暴露了Timer相关的操作
前3个函数的返回值int是用来唯一确定一个定时器的ID,当需要关闭某个Timer的时候,将ID传递给cancelTimer函数即可。IRun是一个回调接口,在mini-muduo里几乎所有的回调都是通过IRun来完成的。runAt的第一个参数是一个代表时间的Timestamp对象,runAfter和runEvery的第一个参数都是以秒为单位的。
4 详细分析下Timer是怎么实现的,其实EventLoop里只有一个Timer文件描述符,当用户通过上面的3个接口向EventLoop添加的所有定时器,实际都工作在同一个timerfd上,这个是怎么做到的呢?我们来跟踪一下EventLoop::runAt()的实现
EventLoop::runAt(...)直接调用了TimerQueue::addTimer()
这里调用了两个方法,一个是insert(...),一个是resetTimerfd(...),在insert(...)里,程序将Timer插入到一个TimerList里,也就是一个std::set<std::pair<Timestamp, Timer*>>,看上去有点繁琐,其实是一个 时间->Timer 键值对的set,这个set的目的是存放所有未到期的定时器,每当timerfd到时,就从里面取出最近的一个定时器,然后修改timerfd把定时器修改成set里下一个最近的时间,这样实现了只使用一个timerfd来管理多个定时器的功能。insert的返回值是个布尔型,意义是新加入的这个定时器是否否比整个set里所有定时器发生的还要早,如果是的话,就必须立刻修改timerfd,将timerfd的定时时间改成这个最近的定时器。如果新加入的定时器不是set里最先发生的定时器,则不用修改timerfd了。
几个实现细节:
细节1:注意定时器容器的选择,也就是TimerQueue里的_timers成员变量,muduo选择了使用二叉搜索树,也就是std::map或者std::set,这两者二选一的过程中,作者最终选择了set,因为同一个时间点下可能有多个定时器,所以直接使用map是不合适的,因为一个pair<Timestamp, Timer*>代表着一个时间,如果直接用map<Timestamp, *Timer>只能保证一个时间点下只对应一个Timer*,这不符合要求,所以作者使用了稍微复杂的类型set<pair<Timestamp, Timer*>> 作为存储定时器的容器。mini-muduo在返回值方面进行了简化,没有像muduo一样返回一个TimerId,而是直接返回了int,其实就是Timer对象的地址。由于在同一个进程里,对象地址是不可能相同的,这样就简单粗暴解决了同样时间下多个定时器的问题。
细节2:定时器的插入操作,使用insert加入到set后,就自动按照Timestamp排序了,Iterator的begin的first就是所有定时器里最早的那一个。如果要插入新的定时器,有个重要的标志,就是earliestTimerChanged,重要条件作为判断,如果(it == end || when < it.begin.first)条件满足就代表标志改变了。
细节3:注意Timestamp的运算符重载
细节4:Timestamp::tostring()方法,要打印64位整数,在32/64平台上可以使用下面的方法
细节5:Interval和expiration的名字应该是来源于man timerfd_settime的i解释
细节6:修改IRun接口,原有的不满足要求,因为不能保存调用参数。同时由于修改了IRun接口,所以在EvenrLoop里添加了Runner类,用于存储回调和参数,原始的_pendingFunctors只能存储回调,参数没地方存。
细节7:由于还没太读懂muduo的cancelTimers队列所以本版本暂时不实现了。