原文链接:

ev_periodic

Libev源码分析05:Libev中的绝对时间定时器 - gqtc - 博客园

ev_periodic和ev_timer也是一种定时器,有时它们是可以通用的,有时也是有区别的。ev_timer不适用于长时间的超时,比如一周后、一个月后,它在一定程度上有延时的风险,回调的超时越大,这个时间就越不准。其实从命名上也能看出区别,ev_periodic适用于周期性的回调,比如每天早上5点需要清理冗余的数据。ev_periodic它是根据时刻来回调的,也就是说,只有本地时钟刚好走到了那个点才会触发回调。

考虑一下特殊情况,如果设置了10分钟后的回调,再把本地时钟调到去年,那么这个回调就要等1年的时间了。如果把时钟往后调呢?这个回调就会立即触发。

可使用的函数如下:

ev_periodic_init(ev_periodic *, callback, ev_tstamp offset, ev_tstamp interval, reschedule_cb)
ev_periodic_set(ev_periodic *, ev_tstamp offset, ev_tstamp interval, reschedule_cb)
ev_periodic_again(loop, ev_periodic *)
ev_tstamp ev_periodic_at(ev_periodic *)

符号定义:

typedef double ev_tstamp;

#if EV_MINPRI == EV_MAXPRI
# define EV_DECL_PRIORITY
#elif !defined (EV_DECL_PRIORITY)
# define EV_DECL_PRIORITY int priority;
#endif

#ifndef EV_COMMON
# define EV_COMMON void *data;
#endif

#ifndef EV_CB_DECLARE
# define EV_CB_DECLARE(type) void (*cb)(EV_P_ struct type *w, int revents);
#endif

#define EV_WATCHER(type)				\
  int active; /* private */				\
  int pending; /* private */			\
  EV_DECL_PRIORITY /* private */		\
  EV_COMMON /* rw */					\
  EV_CB_DECLARE (type) /* private */

#define EV_WATCHER_TIME(type)	\
  EV_WATCHER (type)				\
  ev_tstamp at;     /* private */

#ifdef __cplusplus
# define EV_CPP(x) x
# if __cplusplus >= 201103L
#  define EV_THROW noexcept
# else
#  define EV_THROW throw ()
# endif
#else
# define EV_CPP(x)
# define EV_THROW
#endif

typedef struct ev_periodic
{
	EV_WATCHER_TIME (ev_periodic)
	ev_tstamp offset; /* rw */
	ev_tstamp interval; /* rw */
	ev_tstamp (*reschedule_cb)(struct ev_periodic *w, ev_tstamp now) EV_THROW; /* rw */
} ev_periodic;

#sample1

这个例子每5秒就调用一次clock_cb。

void clock_cb(struct ev_loop *loop, ev_periodic *w, int revents)
{
    printf("clock_cb\n");
}
int main()
{
    struct ev_loop *loop = EV_DEFAULT;

    ev_periodic tick;
    ev_periodic_init(&tick, clock_cb, 0., 5., 0);
    ev_periodic_start(loop, &tick);

    ev_run (loop, 0);
    return 0;
}

#sample2

这个例子和sample1一样,不过使用到了ev_periodic_init的最后一个参数,规则略微复杂。当执行ev_run之后立刻调用了my_scheduler_cb计算出下一次调用clock_cb的时间,此后,每次调用clock_cb之前都会调用my_scheduler_cb计算下一次回调clock_cb的时间。除了首次之外,它们都是依次调用的。这种写法可以满足不定长超时的回调,在my_scheduler_cb里边计算下次回调的时间即可。

严格来讲,这个例子的执行顺序是这样的。第0秒,调用my_scheduler_cb。第5秒,调用my_scheduler_cb后立即调用clock_cb。第10秒,调用my_scheduler_cb后立即调用clock_cb。依此类推。

注:ev_periodic_init设置了末参数的话,第3、4个参数就自动失效。

void clock_cb(struct ev_loop *loop, ev_periodic *w, int revents)
{
    printf("clock_cb\n");
}
ev_tstamp my_scheduler_cb(ev_periodic *w, ev_tstamp now)
{
    printf("my_scheduler_cb\n");
    return now + 5;
}
int main()
{
    struct ev_loop *loop = EV_DEFAULT;

    ev_periodic tick;
    ev_periodic_init(&tick, clock_cb, 0., 0., my_scheduler_cb);
    ev_periodic_start(loop, &tick);

    ev_run(loop, 0);
    return 0;
}

sample3

下面这个例子在整点的时候就会回调clock_cb

#include <math.h>
void clock_cb(struct ev_loop *loop, ev_periodic *w, int revents)
{
    printf("clock_cb\n");
}
int main()
{
    struct ev_loop *loop = EV_DEFAULT;

    ev_periodic tick;
    ev_periodic_init(&tick, clock_cb, fmod(ev_now(loop), 3600.), 3600., 0);
    ev_periodic_start(loop, &tick);

    ev_run (loop, 0);
    return 0;
}

Periodic监视器也是libev中定时器的一种,其功能很多,但实现较为复杂。与ev_timer不同,periodic监视器不是基于实时(或相对时间,即过去的物理时间),而是基于钟表时间(绝对时间,及系统钟表过去的事件)。不同的是,钟表时间可能比实时时间跑得快或慢,时间跳跃并不罕见(例如,当你调整钟表时间),通常更适合长时间的计时。

可以让periodic监视器在某个特定时间点后触发:例如,如果希望“在10秒内”触发(通过指定例如ev_now () + 10)。也就是说,绝对时间而不是延迟),然后将系统时钟重置为前一年的1月,则需要一年或更长时间来触发事件(不像ev_timer,它在启动后仍会触发大约10秒,因为它使用相对超时)。

periodic监视器也可以用来实现更复杂的计时器,比如在每个“当地时间的午夜”触发一个事件,或者其他复杂的规则。这一点ev_timer watcher很难做到,因为无法对时间跳跃做出反应。

与计时器一样,回调保证只在应该触发的时间点过去时才被调用。如果多个定时器在同一个循环迭代中准备好了,那么具有更早超时值的定时器将在具有更晚超时值的定时器之前被调用(当回调递归调用ev_run时,情况就不同了)

主要函数:

ev_periodic_init (ev_periodic *, callback, ev_tstamp offset, ev_tstamp interval, reschedule_cb)

ev_periodic_set (ev_periodic *, ev_tstamp offset, ev_tstamp interval, reschedule_cb)

很多参数,实际上有三种操作模式,我们直接介绍操作模式,将从最简单到最复杂来解释:

1)绝对时间定时器:absolute timer (offset = absolute time, interval = 0, reschedule_cb = 0)

在这种配置中,检测器在时钟时间offset过去后触发一个事件。当发生时间跳跃时,它不会重复也不会调整,也就是说,如果它在2020年1月1日运行,那么当系统时钟达到或超过该时间点+offset时,它将被停止并调用。

2)重复间隔定时器:repeating interval timer (offset = offset within interval, interval > 0, reschedule_cb = 0)

在这种模式下,监视器将总是被安排在下一个(offset + N * interval)时间超时(N为整数,它也可以是负的),然后重复,而不管任何时间跳跃,偏移参数仅仅是间隔周期的偏移。可用于创建不随系统时钟漂移的定时器,例如,创建一个每小时触发的ev_periodic:

ev_periodic_set (&periodic, 0., 3600., 0);
这并不意味着两次触发之间总是有3600秒的间隔,而只是当系统时间显示为一个完整小时(UTC)时,或者更准确地说,当系统时间可被3600整除时,才会调用回调。另一种更偏向数学的解释是ev_periodic将在下一个满足time = offset (mod interval)的时间运行回调,而不考虑任何时间跳跃。interval必须是正的,并且为了数值稳定,interval应该大于1/8192(大约100微秒),offset应该大于0,并且最多应该具有与当前时间相似的幅度(比如说,在10倍之内)。offset的典型值实际上是0或介于0和interval之间的值,这也是推荐的范围。还要注意,定时器触发的频率是有上限的(例如,CPU速度),所以如果间隔很小,那么定时稳定性当然会下降。Libev本身试图精确到大约一毫秒(如果操作系统支持并且机器足够快)。

3)手动重安排模式: manual reschedule mode (offset ignored, interval ignored, reschedule_cb = callback)

它必须根据过去的时间值(即大于第二个参数的最低时间值)返回下一次触发的时间。它通常在回调被触发之前被调用,但也可能在其他时间被调用。 注意:这个回调必须总是返回一个大于或等于现在传递值的时间。 这可以用来创建非常复杂的计时器,例如在“下一个午夜,当地时间”触发的计时器。要做到这一点,需要计算从现在起的下一个午夜,并返回时间戳值。下面是一个关于如何做到这一点的例子:

#include

static ev_tstamp
my_rescheduler (ev_periodic *w, ev_tstamp now)
{

 time_t tnow = (time_t)now;
 struct tm tm;
 localtime_r (&tnow, &tm);

 tm.tm_sec = tm.tm_min = tm.tm_hour = 0; // midnight current day
 ++tm.tm_mday; // midnight next day

 return mktime (&tm);

}
最后给出一个例子很好的说明三种基本方法的使用:

#include <ev.h>
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <math.h> // for fmod
 
#define TIMEOUT 4.
struct ev_loop *loop = NULL;
ev_periodic periodic_watcher;
 
static void periodic_cb(struct ev_loop *loop, ev_periodic *w, int revents)
{
        printf("periodic_cb() call\n");
}
 
static ev_tstamp scheduler_cb(ev_periodic *w, ev_tstamp now)
{
        double mod = fmod(now, TIMEOUT);
        printf("scheduler_cb() call, now = %lf, mod = %lf\n", now, mod);
        return now + (TIMEOUT - mod);
}
 
void *ev_periodic_create(void *p)
{
        loop = ev_loop_new(EVFLAG_AUTO);
 
        // 下面三种初始化方法都可以,实现的效果是一样的。
        //ev_periodic_init(&periodic_watcher, periodic_cb, 0., TIMEOUT, 0); //  periodic_cb每隔TIMEOUT秒被调用一次,对应基本方法1.
        //ev_periodic_init(&periodic_watcher, periodic_cb, fmod(ev_now(loop), TIMEOUT), TIMEOUT, 0); // periodic_cb每隔TIMEOUT秒被调用一次,对应基本方法2.
        ev_periodic_init(&periodic_watcher, periodic_cb, 0., 0., scheduler_cb); // periodic_cb每隔TIMEOUT秒被调用一次,对应基本方法3.
 
         // 需要注意的是对于第三种初始化方法,执行下面这个方法后会主动去调用一次scheduler_cb函数(但此时并不触发periodic_cb函数),以后就是每隔TIMEOUT秒后才调用scheduler_cb,并且触发periodic_cb。
        ev_periodic_start(loop, &periodic_watcher);
        printf("ev_periodic_create() call, after start!\n");
        ev_run(loop, 0);
}
 
int main()
{
        pthread_t tid;
        pthread_create(&tid, NULL, ev_periodic_create, NULL);
        while(1)
        {
                static int count = 0;
                printf("count = %d\n", count++);
                sleep(1);
        }
        return 0;
}

其它函数:

ev_periodic_again (loop, ev_periodic *):

只需停止并重新启动定期观察器。只有当更改了一些参数,或者重新计划回调返回的时间与上次调用的时间不同时(例如,在类似crond的程序中,当crontabs发生更改时),这才是有用的。

ev_tstamp ev_periodic_at (ev_periodic *):

激活时,返回观察者下一次触发的绝对时间。这与ev_periodic_set的offset参数不同,但实际上甚至在间隔和手动重新调度模式下也能工作。

ev_tstamp offset [read-write]:

重复时,它包含偏移值,否则这就是绝对时间点(偏移值传递给ev_periodic_set,尽管libev可能会修改此值以获得更好的数值稳定性)。 可以随时修改,但更改仅在定期计时器触发或再次调用ev _ periodic _时生效。

ev_tstamp interval [read-write]:

当前间隔值。可以随时修改,但更改仅在定期计时器触发或再次调用ev _ periodic _时生效。

ev_tstamp (*reschedule_cb)(ev_periodic *w, ev_tstamp now) [read-write]:

当前的重新计划回调,如果此功能被关闭,则为0。可以随时更改,但更改仅在定期计时器触发或再次调用ev _ periodic _时生效。

Libev源码分析05:Libev中的绝对时间定时器

    Libev中的超时监视器ev_periodic,是绝对时间定时器,不同于ev_timer,它是基于日历时间的。比如如果指定一个ev_periodic在10秒之后触发(ev_now() + 10),然后将系统时间调整为去年的一月一号,则该定时器会在一年后才触发超时事件。(ev_timer依然会在10秒之后触发)

一:数据结构

     超时监视器ev_ periodic结构:

typedef struct ev_periodic
{

int active; 
int pending;
int priority;
void *data;
void (*cb)(struct ev_loop *loop, struct ev_periodic *w, int revents);  
ev_tstamp at;

ev_tstamp offset; /* rw */
ev_tstamp interval; /* rw */
ev_tstamp (*reschedule_cb)(struct ev_periodic *w, ev_tstamp now) EV_THROW; /* rw */

} ev_periodic;

     可见其中的前六个成员与ev_timer和ev_watcher_time是一样的。与ev_timer类似,ev_periodic中的active也标明该监视器在堆数组periodics中的下标;at表明超时事件触发的时间点,共有三种设置方法,而且offset、interval和reschedule_cb都是用来设置触发时间的,这个会在下面说明。

二:监视器函数

1:设置超时监视器

#define ev_periodicset(ev,ofs,ival,rcb) do {
(ev)->offset = (ofs);
(ev)->interval = (ival
);
(ev)->reschedulecb = (rcb);
}while (0)

#define ev_periodic_init(ev,cb,ofs,ival,rcb) do {
ev_init ((ev), (cb));
ev_periodic_set ((ev),(ofs),(ival),(rcb));
} while (0)

2:启动监视器ev_periodic_start

void ev_periodic_start (struct ev_loop *loop, ev_periodic *w)
{

if (expect_false (ev_is_active (w)))
    return;

if (w->reschedule_cb)
    ev_at (w) = w->reschedule_cb (w, ev_rt_now);
else if (w->interval)
{
    assert (("libev: ev_periodic_start called with negative interval value", w->interval >= 0.));
    periodic_recalc (EV_A_ w);
}
else
    ev_at (w) = w->offset;


++periodiccnt;
ev_start (EV_A_ (W)w, periodiccnt + HEAP0 - 1);
array_needsize (ANHE, periodics, periodicmax, ev_active (w) + 1, EMPTY2);
ANHE_w (periodics [ev_active (w)]) = (WT)w;
ANHE_at_cache (periodics [ev_active (w)]);
upheap (periodics, ev_active (w));

}

    共有三种设置超时时间at的方法:

    a:如果reschedule_cb不为空,则忽略interval和offset,而使用reschedule_cb函数设置超时时间at,该函数以ev_rt_now为参数,设置下次超时事件触发的时间,每次重新设置at的时候(periodics_reschedule,periodics_reify),都会调用该函数。该函数的一个例子如下:

static ev_tstamp my_rescheduler (ev_periodic *w, ev_tstamp now)
{

return now + 60.;

}

    这就是将at设置为1分钟之后的时间点。



    b:reschedule_cb为空,interval>0,这种情况下,调用periodic_recalc设置at。该函数的作用就是将at置为下一个的offset + N*interval时间点,其中的offset一般处于[0, interval]范围内。比如置offset为0,interval为3600,意味着当系统时间是完整的1小时的时候,也就是系统时间可以被3600整除的时候,比如8:00,9:00等,就会触发超时事件。periodic_recalc的代码见下面。



    c:如果reschedule_cb为空,interval为0,则直接将at置为offset。这是一种绝对值,这种情况下,该监视器不会重复触发,触发一次之后就会停止监视器;而且该监视器也会无视时间调整,比如置at为20110101000000,则只要系统日历时间超过了改时间,就会触发超时事件。



    设置好at之后,就是将该监视器加入到堆periodics中,这与ev_timer的代码是一样的,不再赘述。

3:periodic_recalc重新计算下一个触发时间点

void periodic_recalc (struct ev_loop *loop, ev_periodic *w)
{

ev_tstamp interval = w->interval > MIN_INTERVAL ? w->interval : MIN_INTERVAL;
ev_tstamp at = w->offset + interval * ev_floor ((ev_rt_now - w->offset) / interval);

while (at <= ev_rt_now)
{
    ev_tstamp nat = at + w->interval;

    if (expect_false (nat == at))
    {
        at = ev_rt_now;
        break;
    }

    at = nat;
}
ev_at (w) = at;

}

     该函数的作用就是将at置为下一个的offset + N*interval时间点。ev_floor(x)返回小于x,且最接近x的整数。

     举个例子可能会容易明白该代码:interval为10分钟(600),offset为2分钟(120),表示将at置为下一个分钟数为2的时间点。

     假设当前为8:01:23,则最终会使得at为8:02:00。计算过程是 :interval * ev_floor ((ev_rt_now - w->offset) / interval)就表示7:50:00,然后再加上offset就是7:52:00,进入循环,最终调整得at=8:02:00。

     假设当前为8:03:56,则最终会使得at为8:12:00。计算过程是:interval * ev_floor ((ev_rt_now -w->offset) / interval)就表示8:00:00,然后再加上offset就是8:02:00,进入循环,最终调整得at=8:12:00。



     4:停止超时监视器ev_periodic_stop

void ev_periodic_stop (struct ev_loop *loop, ev_periodic *w)
{

clear_pending (EV_A_ (W)w);
if (expect_false (!ev_is_active (w)))
    return;

int active = ev_active (w);


--periodiccnt;

if (expect_true (active < periodiccnt + HEAP0))
{
    periodics [active] = periodics [periodiccnt + HEAP0];
    adjustheap (periodics, periodiccnt, active);
}
ev_stop (EV_A_ (W)w);

}

     代码与ev_timer_stop几乎完全一致,不再赘述。



     5:重新调整超时时间periodics_reschedule

static void periodics_reschedule (struct ev_loop *loop)
{

int i;

for (i = HEAP0; i < periodiccnt + HEAP0; ++i)
{
    ev_periodic *w = (ev_periodic *)ANHE_w (periodics [i]);

    if (w->reschedule_cb)
        ev_at (w) = w->reschedule_cb (w, ev_rt_now);
    else if (w->interval)
        periodic_recalc (EV_A_ w);

    ANHE_at_cache (periodics [i]);
}

reheap (periodics, periodiccnt);

}

     在time_update中,如果发现日历时间被调整了,则会调用periodics_reschedule函数,调整ev_periodic的超时时间点at。调整的方法跟ev_periodic_start中的一样,要么使用reschedule_cb函数调整,要么就是调用periodic_recalc重新计算at。最后,将periodics堆中所有元素都调整完毕后,调用reheap使periodics恢复堆结构。



     6:将激活的超时事件排队periodics_reify

void periodics_reify (struct ev_loop *loop)
{

while (periodiccnt && ANHE_at (periodics [HEAP0]) < ev_rt_now)
{
    do{
        ev_periodic *w = (ev_periodic *)ANHE_w (periodics [HEAP0]);

        if (w->reschedule_cb)
        {
            ev_at (w) = w->reschedule_cb (w, ev_rt_now);
            assert (("libev: ev_periodic reschedule callback returned time in the past", ev_at (w) >= ev_rt_now));

            ANHE_at_cache (periodics [HEAP0]);
            downheap (periodics, periodiccnt, HEAP0);
        }
        else if (w->interval)
        {
            periodic_recalc (EV_A_ w);
            ANHE_at_cache (periodics [HEAP0]);
            downheap (periodics, periodiccnt, HEAP0);
        }
        else
            ev_periodic_stop (EV_A_ w); 

        feed_reverse (EV_A_ (W)w);
    }
    while (periodiccnt && ANHE_at (periodics [HEAP0]) < ev_rt_now);

    feed_reverse_done (EV_A_ EV_PERIODIC);
}

}

     主要流程跟timers_reify一样,只不过在重新计算下次触发时间点at的时候,计算方法跟ev_periodic_start中的一样。

三:例子

ev_periodic pw;

void periodic_action(struct ev_loop *main_loop,ev_periodic *timer_w,int e)
{

time_t now;
now = time(NULL);
printf("cur time is %s\n", ctime(&now));

}

static ev_tstamp my_rescheduler (ev_periodic *w, ev_tstamp now)
{

return now+120;

}

int main()
{

time_t now;
now = time(NULL);

struct ev_loop *main_loop = ev_default_loop(0);

ev_periodic_init(&pw, periodic_action, 0, 0, my_rescheduler);   //1
//ev_periodic_init(&pw, periodic_action, 120, 600, NULL);           //2
//ev_periodic_init(&pw, periodic_action, now+20, 0, NULL);      //3
ev_periodic_start(main_loop,&pw);

printf("begin time time is %s\n", ctime(&now));

ev_run(main_loop,0);
return;

}

     采用第一种初始化方法:

ev_periodic_init(&pw, periodic_action, 0, 0, my_rescheduler);

     结果是:

begin time time is Thu Oct 29 21:33:05 2015

cur time is Thu Oct 29 21:35:05 2015
cur time is Thu Oct 29 21:37:05 2015
cur time is Thu Oct 29 21:39:05 2015
cur time is Thu Oct 29 21:41:05 2015
...

     采用第二种初始化方法:

ev_periodic_init(&pw, periodic_action, 120, 600, NULL);

     结果是:

begin time time is Thu Oct 29 21:38:29 2015

cur time is Thu Oct 29 21:42:00 2015
cur time is Thu Oct 29 21:52:00 2015
cur time is Thu Oct 29 22:02:00 2015
cur time is Thu Oct 29 22:12:00 2015
cur time is Thu Oct 29 22:22:00 2015
...

     采用第三种初始化方法:

ev_periodic_init(&pw, periodic_action, now+20, 0, NULL);

     结果是:

begin time time is Thu Oct 29 21:39:03 2015

cur time is Thu Oct 29 21:39:23 2015

`