基本介绍

ev_timer是相对计时器观察者, 通过设置指定的超时时间与可选的重复触发时间.

1. 选择”更好”的超时方式

在现实世界中的许多超时是为了解决某些问题, 例如: http请求时间太长, 我们需要在一定时间后引发异常.

下面有一些简单的示例来说明, 从”简单低效“到”复杂高效“. 例如,每次接收到一些数据时重置下次60秒:

  1. 使用ev_timer_initev_timer_start每次激活计时器:

    这是最明显但不是最简单的方法:

    1
    2
    ev_timer_init (timer, callback, 60., 0.);
    ev_timer_start (loop, timer);

    然后在每次触发后将计时器时间重置:

    1
    2
    3
    ev_timer_stop (loop, timer);
    ev_timer_set (timer, 60., 0.);
    ev_timer_start (loop, timer);

    这样实现非常简单, 但是因为每次需要删除后重新激活. Libev必须将其从最小堆结构中删除后重新添加, 这样的操作几乎很难保证是常量级(constant-time)的.

  2. 同样的启动方式, 但是使用ev_timer_again调整时间:

    最简单的方式就是使用ev_timer_again来代替ev_timer_start, 要实现的话必须直接使用ev_init与指定方式配置一个重复计时器.

    这意味着你可以忽略ev_timer_start函数和ev_timer_setafter参数, 并且只是使用repeat成员与ev_timer_again函数.

    比如, 这样激活计时器:

    1
    2
    3
    ev_init (timer, callback);
    timer->repeat = 60.;
    ev_timer_again (loop, timer);

    甚至随时更改超时, 无论它是否处于活跃状态:

    1
    2
    timer->repeat = 30.;
    ev_timer_again (loop, timer);

    这显然比第1种方式更加高效, 应为这样能避免Libev在内部完全删除数据结构后又重新将其插入进去.

    当然, 这种方式也仅此而已了.

  3. 通过计算相对超时时间, 然后根据需要重置它:

    首先, 计算超时发生所需要的时间(通过计算绝对时间减去相对时间与最后活跃时间). 如果值为负数说明超时已到, 正常处理超时任务即可. 否则我们将时间设置为最早一个等待触发的计时器并且启动.

    换句话说, 每次调用回调的时候都会检查是否发生超时. 如果没有的话, 它只会简单的重新让自己在下一次最早触发的时间点进行检查. 然后重复以上动作. 这个方法需要更多的回调次数, 但实际上不会更改Libev调用来更改超时时间.

    在首次启动的时候, 只需初始化观察者并将最后活跃时间(last_activity)设置为当前时间. 然后调用回调, 启动计时器:

    1
    2
    3
    last_activity = ev_now (EV_A);
    ev_init (&timer, callback);
    callback (EV_A_ &timer, 0);

    当有其中一些超时. 只需要将当前时间记录即可, 而不会实际调用libev更改:

    1
    2
    if (activity detected)
    last_activity = ev_now (EV_A);

    当超时周期更改, 则可以通过简单的参数替换、停止计时器、立即调用回调来解决:

    1
    2
    3
    timeout = new_value;
    ev_timer_stop (EV_A_ &timer);
    callback (EV_A_ &timer, 0);

    这种实现较为复杂, 在超时周期较长的、不太可能真正超时的场景下尤为有用.

  4. 使用排序双向链表:

    如果计时器需求量非常大(成千上万, 甚至数百万), 并且它们都具有某种超时特性(timeout value)那可以做的更好.

    比如: 使用链表头部来作为最近超时计算, 如果发现一些活跃的计时器则可以从中处理并且删除(如果是重复超时则插入到链表尾部). 确保更新ev_timer如果它是从开头获取的, 这样可以以O(1)的复杂度管理近乎无限的(已内存而定)超时操作(启动、停止、更新).

    但是这样的代价则是实现复杂度. 除了保证恒定的超时时间外, 还需要确保链表的有序性.

    哪种方式”最好”的呢?

    方法2几乎简单到无需思考, 在大多数情况下都能满足需求. 方法3需要思考更多但也不会非常复杂. 虽然方法3在普通情况下会更好, 但是这两者选其中任何一个都可以.

    方法1始终不是一个好选择, 并且不会给你带来任何好处. 方法4则非常复杂, 但是会更加有效. (这种有效被认为是过度设计)

2. 非同步时钟的特殊问题

现代操作系统”时钟”多种多样 - Libev 使用普通的挂钟(wall clock)模型运行, 如果可以使用单调时钟(monotonic clock)来避免时间跳跃.

这些时钟都不会与彼此进行同步, 因此ev_time()可能返回与gettimeofday()time()大不相同的时间. 例如,在GNU/Linux系统上,调用他们之间的差值可能会高1秒.

由于时间不同步, 还会出现另一个问题: Libev使用的是单调时钟(monotonic clock), 从启动计时器到回调的期间您比较 ev_timeev_now的时间戳, 就会发现回调被提前调用了.

这是因为ev_timer参照实际时间而非挂钟(wall clock)时间, 所以Libev必须确保回调必须在时间到来之前没被调用. 测量参考的是实际时间而非系统时钟. 如果恰好您是基于物理时间尺度计算超时(例如: “在100秒后超时连接”), 那么这对您来说应该是正确的行为.

3. “假死”的特殊问题

当您遇到挂起、休眠等机器, 这期间时间会发生什么变化呢?

使用Linux 2.6.28进行的一些快速测试表明: 挂起(suspend)会暂停所有进程, 而时钟(CLOCK_MONOTONIC)会继续运行直到系统从挂起中恢复.

这意味着恢复后, 对程序来说就想只过去了几秒钟. 而如果此时使用单调时钟(monotonic clock)源, 暂停期间的事件则不会被计入ev_timer当中. 如果使用实时时钟(Real Time)超时则会被提前, 并且Libev会检测到挂起并调整好计时器.

在不同的操作系统、操作系统版本甚至不同的硬件上看到不同的行为.

4. 时间更新的特殊问题

获取当前时间是一个昂贵的操作(至少需要一个系统调用周期): 因此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()返回的时间. 尽管这样做会将后续事件继续推迟.

相关函数

ev_timer_init (ev_timer *, callback, ev_tstamp after, ev_tstamp repeat)

ev_timer_set (ev_timer *, ev_tstamp after, ev_tstamp repeat)

将计时器配置为在after秒后触发(支持小数和负值). 如果repeat0., 那么超时时间一到则会自动停止. 否则计时器会自动配置为在repeat秒后无限重复触发, 直到它被主动调用停止.

ev_timer_again (loop, ev_timer *)

它的所有确切语义如下所示:

  1. 如果计时器已经挂起(pending)待处理, 调用此函数则清除状态.

  2. 如果计时器已启动但不是循环计时器, 调用此函数则会停止它.

  3. 如果计时器正在重复执行期间, 调用此函数则会根据repeat值重新设置重复时间并启动.

这听起来有点复杂, 可以参考前面的描述仔细品味.

ev_tstamp ev_timer_remaining (loop, ev_timer *)

返回计时器触发前的剩余时间. 如果计时器处于活动状态,那么这个时间是相对于当前事件循环时间的,否则就是当前配置的超时值.
例如: 在调用ev_timer_set(w, 5, 7)之后ev_timer_remaining返回5, 当计时器启动并经过1秒后ev_timer_remaining返回的是4. 当计时器到期重新启动时它会返回大约7(左右)等等.

ev_tstamp repeat [read-write]

repeat表示每次重复超时的值. 将在每次观察者超时或调用ev_timer_again时使用, 并确定下一次超时(如果有的话),这也是考虑任何修改时.

使用示例

  1. 创建一个3秒后超时的示例:
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
// 只需导入单个头文件
#include <ev.h>
#include <stdio.h>
#include <unistd.h>

// 当超时时间到达, 这个回调将会被触发.
static void timeout_cb (struct ev_loop *loop, ev_timer *w, int revents)
{
puts ("timeout");
}


int main (void)
{
// 可以使用已定义的宏来获取默认的事件循环, 当然你也可以根据自己的需求创建指定的.
struct ev_loop *loop = EV_DEFAULT;

ev_timer timeout_watcher;
ev_timer_init (&timeout_watcher, timeout_cb, 3., 0.);
ev_timer_start (loop, &timeout_watcher);

// 开始运行事件循环
ev_run (loop, 0);

// 如果事件循环退出, 那将会执行到这里.
return 0;
}
  1. 创建一个3秒后超时, 之后每隔1秒超时的示例:
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
// 只需导入单个头文件
#include <ev.h>
#include <stdio.h>
#include <unistd.h>

// 当超时时间到达, 这个回调将会被触发.
static void timeout_cb (struct ev_loop *loop, ev_timer *w, int revents)
{
puts ("timeout");
}


int main (void)
{
// 可以使用已定义的宏来获取默认的事件循环, 当然你也可以根据自己的需求创建指定的.
struct ev_loop *loop = EV_DEFAULT;

ev_timer timeout_watcher;
ev_timer_init (&timeout_watcher, timeout_cb, 3., 1.);
ev_timer_start (loop, &timeout_watcher);

// 开始运行事件循环
ev_run (loop, 0);

// 如果事件循环退出, 那将会执行到这里.
return 0;
}