ev_timer
是相对计时器观察者, 通过设置指定的超时时间与可选的重复触发时间.
在现实世界中的许多超时是为了解决某些问题, 例如: http
请求时间太长, 我们需要在一定时间后引发异常.
下面有一些简单的示例来说明, 从”简单低效“到”复杂高效“. 例如,每次接收到一些数据时重置下次60
秒:
使用ev_timer_init
与ev_timer_start
每次激活计时器:
这是最明显但不是最简单的方法:
1 | ev_timer_init (timer, callback, 60., 0.); |
然后在每次触发后将计时器时间重置:
1 | ev_timer_stop (loop, timer); |
这样实现非常简单, 但是因为每次需要删除后重新激活. Libev
必须将其从最小堆结构中删除后重新添加, 这样的操作几乎很难保证是常量级(constant-time
)的.
同样的启动方式, 但是使用ev_timer_again
调整时间:
最简单的方式就是使用ev_timer_again
来代替ev_timer_start
, 要实现的话必须直接使用ev_init
与指定方式配置一个重复计时器.
这意味着你可以忽略ev_timer_start
函数和ev_timer_set
的after
参数, 并且只是使用repeat
成员与ev_timer_again
函数.
比如, 这样激活计时器:
1 | ev_init (timer, callback); |
甚至随时更改超时, 无论它是否处于活跃状态:
1 | timer->repeat = 30.; |
这显然比第1
种方式更加高效, 应为这样能避免Libev
在内部完全删除数据结构后又重新将其插入进去.
当然, 这种方式也仅此而已了.
通过计算相对超时时间, 然后根据需要重置它:
首先, 计算超时发生所需要的时间(通过计算绝对时间减去相对时间与最后活跃时间). 如果值为负数说明超时已到, 正常处理超时任务即可. 否则我们将时间设置为最早一个等待触发的计时器并且启动.
换句话说, 每次调用回调的时候都会检查是否发生超时. 如果没有的话, 它只会简单的重新让自己在下一次最早触发的时间点进行检查. 然后重复以上动作. 这个方法需要更多的回调次数, 但实际上不会更改Libev
调用来更改超时时间.
在首次启动的时候, 只需初始化观察者并将最后活跃时间(last_activity
)设置为当前时间. 然后调用回调, 启动计时器:
1 | last_activity = ev_now (EV_A); |
当有其中一些超时. 只需要将当前时间记录即可, 而不会实际调用libev
更改:
1 | if (activity detected) |
当超时周期更改, 则可以通过简单的参数替换、停止计时器、立即调用回调来解决:
1 | timeout = new_value; |
这种实现较为复杂, 在超时周期较长的、不太可能真正超时的场景下尤为有用.
使用排序双向链表:
如果计时器需求量非常大(成千上万, 甚至数百万), 并且它们都具有某种超时特性(timeout value
)那可以做的更好.
比如: 使用链表头部来作为最近超时计算, 如果发现一些活跃的计时器则可以从中处理并且删除(如果是重复超时则插入到链表尾部). 确保更新ev_timer
如果它是从开头获取的, 这样可以以O(1)
的复杂度管理近乎无限的(已内存而定)超时操作(启动、停止、更新).
但是这样的代价则是实现复杂度. 除了保证恒定的超时时间外, 还需要确保链表的有序性.
哪种方式”最好”的呢?
方法2
几乎简单到无需思考, 在大多数情况下都能满足需求. 方法3
需要思考更多但也不会非常复杂. 虽然方法3
在普通情况下会更好, 但是这两者选其中任何一个都可以.
方法1
始终不是一个好选择, 并且不会给你带来任何好处. 方法4
则非常复杂, 但是会更加有效. (这种有效被认为是过度设计)
现代操作系统”时钟”多种多样 - Libev
使用普通的挂钟(wall clock
)模型运行, 如果可以使用单调时钟(monotonic clock
)来避免时间跳跃.
这些时钟都不会与彼此进行同步, 因此ev_time()
可能返回与gettimeofday()
或time()
大不相同的时间. 例如,在GNU/Linux
系统上,调用他们之间的差值可能会高1
秒.
由于时间不同步, 还会出现另一个问题: Libev
使用的是单调时钟(monotonic clock
), 从启动计时器到回调的期间您比较 ev_time
或ev_now
的时间戳, 就会发现回调被提前调用了.
这是因为ev_timer
参照实际时间而非挂钟(wall clock
)时间, 所以Libev
必须确保回调必须在时间到来之前没被调用. 测量参考的是实际时间而非系统时钟. 如果恰好您是基于物理时间尺度计算超时(例如: “在100
秒后超时连接”), 那么这对您来说应该是正确的行为.
当您遇到挂起、休眠等机器, 这期间时间会发生什么变化呢?
使用Linux 2.6.28
进行的一些快速测试表明: 挂起(suspend
)会暂停所有进程, 而时钟(CLOCK_MONOTONIC
)会继续运行直到系统从挂起中恢复.
这意味着恢复后, 对程序来说就想只过去了几秒钟. 而如果此时使用单调时钟(monotonic clock
)源, 暂停期间的事件则不会被计入ev_timer
当中. 如果使用实时时钟(Real Time)超时则会被提前, 并且Libev
会检测到挂起并调整好计时器.
在不同的操作系统、操作系统版本甚至不同的硬件上看到不同的行为.
获取当前时间是一个昂贵的操作(至少需要一个系统调用周期): 因此Libev
仅在ev_run
收集新事件之前和之后更新其当前时间,这导致在一次迭代中处理大量事件时, ev_now()
和ev_time()
之间的差异越来越大.
相对超时是根据ev_now()
时间计算的. 这通常是正确的,因为这个时间戳指的是触发您正在修改/启动的超时的事件的时间。如果你怀疑事件处理被延迟,你需要基于当前时间的超时,使用如下的东西来调整它:
1 | ev_timer_set (&timer, after + (ev_time () - ev_now ()), 0.); |
如果事件循环被长期暂停, 您也可以使用ev_now_update()
来强制更新ev_now()
返回的时间. 尽管这样做会将后续事件继续推迟.
将计时器配置为在
after
秒后触发(支持小数和负值). 如果repeat
为0.
, 那么超时时间一到则会自动停止. 否则计时器会自动配置为在repeat
秒后无限重复触发, 直到它被主动调用停止.
它的所有确切语义如下所示:
如果计时器已经挂起(
pending
)待处理, 调用此函数则清除状态.如果计时器已启动但不是循环计时器, 调用此函数则会停止它.
如果计时器正在重复执行期间, 调用此函数则会根据
repeat
值重新设置重复时间并启动.
这听起来有点复杂, 可以参考前面的描述仔细品味.
返回计时器触发前的剩余时间. 如果计时器处于活动状态,那么这个时间是相对于当前事件循环时间的,否则就是当前配置的超时值.
例如: 在调用ev_timer_set(w, 5, 7)
之后ev_timer_remaining
返回5
, 当计时器启动并经过1
秒后ev_timer_remaining
返回的是4
. 当计时器到期重新启动时它会返回大约7
(左右)等等.
repeat
表示每次重复超时的值. 将在每次观察者超时或调用ev_timer_again
时使用, 并确定下一次超时(如果有的话),这也是考虑任何修改时.
3
秒后超时的示例:1 | // 只需导入单个头文件 |
3
秒后超时, 之后每隔1
秒超时的示例:1 | // 只需导入单个头文件 |