ev_timer

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

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

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

  1. 使用ev_timer_init与ev_timer_start每次激活计时器:

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

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

    ev_timer_stop (loop, timer);
    ev_timer_set (timer, 60., 0.);
    ev_timer_start (loop, timer);
    
  2. 同样的启动方式, 但是使用ev_timer_again调整时间:

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

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

    比如, 这样激活计时器:

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

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

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

    这显然比第1种方式更加高效

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

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

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

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

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

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

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

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

    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则非常复杂, 但是会更加有效. (这种有效被认为是过度设计)

非同步时钟的特殊问题

现代操作系统”时钟”多种多样 -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()时间计算的. 这通常是正确的,因为这个时间戳指的是触发您正在修改/启动的超时的事件的时间。如果你怀疑事件处理被延迟,你需要基于当前时间的超时,使用如下的东西来调整它:

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);

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

// 它的所有确切语义如下所示:
//  1. 如果计时器已经挂起(pending)待处理, 调用此函数则清除状态.
//  2. 如果计时器已启动但不是循环计时器, 调用此函数则会停止它.
//  3. 如果计时器正在重复执行期间, 调用此函数则会根据repeat值重新设置重复时间并启动.
ev_timer_again (loop, ev_timer *);

// 返回计时器触发前的剩余时间. 如果计时器处于活动状态,那么这个时间是相对于当前事件循环时间的,否则就是当前配置的超时值.
ev_tstamp ev_timer_remaining (loop, ev_timer *);

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

例子

创建一个3秒后超时的示例:

// 只需导入单个头文件
#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;
}

创建一个3秒后超时, 之后每隔1秒超时的示例:

// 只需导入单个头文件
#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;
}