当前位置:编程学习 > C/C++ >>

Muduo网络编程示例之三:定时器

程序中的时间
程序中对时间的处理是个大问题,我打算单独写一篇文章来全面地讨论这个问题。文章暂定名《〈程序中的日期与时间〉第二章 计时与定时》,跟《〈程序中的日期与时间〉第一章 日期计算》放到一个系列,这个系列预计会有四篇文章。

在这篇博客里里我先简要谈谈与编程直接相关的内容,把更深入的内容留给上面提到的日期与时间专题文章。

在一般的服务端程序设计中,与时间有关的常见任务有:

获取当前时间,计算时间间隔;
时区转换与日期计算;把纽约当地时间转换为上海当地时间;2011-02-05 之后第 100 天是几月几号星期几?等等
定时操作,比如在预定的时间执行一项任务,或者在一段延时之后执行一项任务。
其中第 2 项看起来复杂,其实最简单。日期计算用 Julian Day Number,时区转换用 tz database;惟一麻烦一点的是夏令时,但也可以用 tz database 解决。这些操作都是纯函数,很容易用一套单元测试来验证代码的正确性。需要特别注意的是,用 tzset/localtime_r 来做时区转换在多线程环境下可能会有问题;对此我的解决办法是写一个 TimeZone class,以避免影响全局,将来在日期与时间专题中会讲到。以下本文不考虑时区,均为 UTC 时间。

真正麻烦的是第 1 项和第 3 项。一方面,Linux 有一大把令人眼花缭乱的与时间相关的函数和结构体,在程序中该如何选用?另一方面,计算机中的时钟不是理想的计时器,它可能会漂移或跳变;最后,民用的 UTC 时间与闰秒的关系也让定时任务变得复杂和微妙。当然,与系统当前时间有关的操作也让单元测试变得困难。

Linux 时间函数
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 (这些与当前时间无关)
定时函数,用于让程序等待一段时间或安排计划任务:

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 的主要设计考虑)

time 的精度太低,ftime 已被废弃,clock_gettime 精度最高,但是它系统调用的开销比 gettimeofday 大。
在 x86-64 平台上,gettimeofday 不是系统调用,而是在用户态实现的(搜 vsyscall),没有上下文切换和陷入内核的开销。
gettimeofday 的分辨率 (resolution) 是 1 微秒,足以满足日常计时的需要。muduo::Timestamp 用一个 int64_t 来表示从 Epoch 到现在的微秒数,其范围可达上下 30 万年。
timerfd_* 入选的原因:

sleep / alarm / usleep 在实现时有可能用了信号 SIGALRM,在多线程程序中处理信号是个相当麻烦的事情,应当尽量避免。(近期我会写一篇博客仔细讲讲“多线程、RAII、fork() 与信号”)
nanosleep 和 clock_nanosleep 是线程安全的,但是在非阻塞网络编程中,绝对不能用让线程挂起的方式来等待一段时间,程序会失去响应。正确的做法是注册一个时间回调函数。
getitimer 和 timer_create 也是用信号来 deliver 超时,在多线程程序中也会有麻烦。timer_create 可以指定信号的接收方是进程还是线程,算是一个进步,不过在信号处理函数(signal handler)能做的事情实在很受限。
timerfd_create 把时间变成了一个文件描述符,该“文件”在定时器超时的那一刻变得可读,这样就能很方便地融入到 select/poll 框架中,用统一的方式来处理 IO 事件和超时事件,这也正是 Reactor 模式的长处。我在一年前发表的《Linux 新增系统调用的启示》中也谈到这个想法,现在我把这个想法在 muduo 网络库中实现了。
传统的 Reactor 利用 select/poll/epoll 的 timeout 来实现定时功能,但 poll 和 epoll 的定时精度只有毫秒,远低于 timerfd_settime 的定时精度。
必须要说明,在 Linux 这种非实时多任务操作系统中,在用户态实现完全精确可控的计时和定时是做不到的,因为当前任务可能会被随时切换出去,这在 CPU 负载大的时候尤为明显。但是,我们的程序可以尽量提高时间精度,必要的时候通过控制 CPU 负载来提高时间操作的可靠性,在程序在 99.99% 的时候都是按预期执行的。这或许比换用实时操作系统并重新编写并测试代码要经济一些。

关于时间的精度(accuracy)问题我留到专题博客文章中讨论,它与分辨率(resolution)不完全是一回事儿。时间跳变和闰秒的影响与应对也不在此处展开讨论了。

Muduo 的定时器接口
Muduo EventLoop 有三个定时器函数:

   1: typedef boost::function<void()> TimerCallback;   2:     3: ///   4: /// Reactor, at most one per thread.   5: ///   6: /// This is an inte易做图ce class, so dont expose too much details.   7: class EventLoop : boost::noncopyable   8: {   9:  public:  10:   // ...  11:    12:   // timers  13:    14:   ///  15:   TimerId runAt(const Timestamp& time, const TimerCallback& cb);  16:    17:   ///  18:   /// Runs callback after @c delay seconds.  19:   /// Safe to call from other threads.  20:   TimerId runAfter(double delay, const TimerCallback& cb);  21:    22:   ///  23:   /// Runs callback every @c interval seconds.  24:   /// Safe to call from other threads.  25:   TimerId runEvery(double interval, const TimerCallback& cb);  26:    27:   /// Cancels the timer.  28:   /// Safe to call from other threads.  29:   // void cancel(TimerId timerId);  30:    31:   // ...  32: };
runAt 在指定的时间调用 TimerCallback
runAfter 等一段时间调用 TimerCallback
runEvery 以固定的间隔反复调用 TimerCallback
cancel 取消 timer,目前未实现
回调函数在 EventLoop 对象所在的线程发生,与 onMessage() onConnection() 等网络事件函数在同一个线程。

Muduo 的 TimerQueue 采用了最简单的实现(链表)来管理定时器,它的效率比不上常见的 binary heap 的做法,如果程序中大量(10 个以上)使用重复触发的定时器,或许值得考虑改用更高级的实现。我目前还没有在一个程序里用过这么多定时器,暂时也不打算优化 TimerQueue。

Boost.Asio Timer 示例
Boost.Asio 教程里以 Timer 和 Daytime 为例介绍 asio 的基本使用,daytime 已经在前文“示例一”中介绍过,这里着重谈谈 Timer。Asio 有 5 个 Timer 示例,muduo 把其中四个重新实现了一遍,并扩充了第 5 个示例。

阻塞式的定时,muduo 不支持这种用法,无代码。
非阻塞定时,见 examples/asio/tutorial/timer2
在 TimerCallback 里传递参数,见 examples/asio/tutorial/timer3
以成员函数为 TimerCallback,见 examples/asio/tutorial/timer4
在多线程中回调,用 mutex 保护共享变量,见 examples/asio/tutorial/timer5
在多线程中回调,缩小临界区,把不需要互斥执行的代码移出来,见 examples/asio/tutorial/timer6
为节省篇幅,这里只列出 timer4:

   1: #include    2:     3: #include    4: #include    5: #include    6:     7: class Printer : boost::noncopyable   8: {   9:  public:  10:   Printer(muduo::net::EventLoop* loop)  11:     : loop_(loop),  12:       count_(0)  13:   {  14:     loop_->runAfter(1, boost::bind(&Printer::print, this));  15:   }  16:    17:   ~Printer()  18:   {  19:     std::cout << "Final count is " << count_ << " ";  20:   }  21:    22:   void print()  23:   {  24:     if (count_ < 5)  25:     {  26:       std::cout << count_ << " ";  27:       ++count_;  28:    29:       loop_->runAfter(1, boost::bind(&Printer::print, this));  30:     }  31:     else  32:     {  33:   &n

补充:软件开发 , C语言 ,
CopyRight © 2022 站长资源库 编程知识问答 zzzyk.com All Rights Reserved
部分文章来自网络,