【问题标题】:Why is libuv timer drifting?为什么 libuv 计时器会漂移?
【发布时间】:2017-07-08 00:23:42
【问题描述】:

在尝试使用 libuv 实现一个简单的计时器时,我注意到回调是“漂移”的。我在这里做错了什么?

代码:

uv_timer_t timer;
uv_timer_init(uv_default_loop(), &timer);

double ts = std::chrono::high_resolution_clock::now().time_since_epoch().count();
timer.data = &ts;

uv_timer_start(&timer,
               [](uv_timer_t* handle) {
                 double* before = (double*)handle->data;
                 double now =
                     std::chrono::high_resolution_clock::now().time_since_epoch().count();
                 auto diff = now - (*before);
                 is::log::info("tick... diff={}ns", diff);
                 *before = now;
               },
               1000, 1000);

uv_run(uv_default_loop(), UV_RUN_DEFAULT);

输出 [libuv]: (diff 总是大于 1.0s)

[info][10:48:26:636439] tick... diff=1.00083e+09ns
[info][10:48:27:637071] tick... diff=1.00063e+09ns
[info][10:48:28:638155] tick... diff=1.00108e+09ns
[info][10:48:29:639219] tick... diff=1.00106e+09ns
[info][10:48:30:640291] tick... diff=1.00107e+09ns
[info][10:48:31:641364] tick... diff=1.00107e+09ns
[info][10:48:32:641457] tick... diff=1.00009e+09ns
[info][10:48:33:642468] tick... diff=1.00101e+09ns
[info][10:48:34:643621] tick... diff=1.00115e+09ns
[info][10:48:35:644701] tick... diff=1.00108e+09ns

等效的 libev 代码完美运行。

代码:

ev_timer timer;

double ts = std::chrono::high_resolution_clock::now().time_since_epoch().count();
timer.data = &ts;

ev_timer_init(&timer,
              [](struct ev_loop*, ev_timer* handle, int) {
                double* before = (double*)handle->data;
                double now =
                    std::chrono::high_resolution_clock::now().time_since_epoch().count();
                auto diff = now - (*before);
                is::log::info("tick... diff={}ns", diff);
                *before = now;
              },
              1.0, 1.0);

ev_timer_start(EV_DEFAULT, &timer);
ev_run(EV_DEFAULT, 0);

输出[libev]:

[info][10:54:01:624788] tick... diff=1.00015e+09ns
[info][10:54:02:624943] tick... diff=1.00016e+09ns
[info][10:54:03:625035] tick... diff=1.00009e+09ns
[info][10:54:04:625177] tick... diff=1.00014e+09ns
[info][10:54:05:624284] tick... diff=9.99106e+08ns
[info][10:54:06:624415] tick... diff=1.00013e+09ns
[info][10:54:07:624533] tick... diff=1.00012e+09ns
[info][10:54:08:624592] tick... diff=1.00006e+09ns
[info][10:54:09:625245] tick... diff=1.00065e+09ns
[info][10:54:10:624331] tick... diff=9.99086e+08ns

解决方案

实现了我自己的计时器补偿逻辑,该逻辑在每次迭代时以“正确”超时启动一个新计时器。

using namespace std::chrono;

struct Timer {
  uv_loop_t* loop;
  uv_timer_t timer;
  uint64_t period;
  time_point<high_resolution_clock> ref;

  Timer(uv_loop_t* loop, uint64_t period)
      : loop(loop), period(period), ref(high_resolution_clock::now() + milliseconds(period)) {
    uv_timer_init(loop, &timer);
    timer.data = this;
    uv_timer_start(&timer, callback, period, 0);
  }

  static void callback(uv_timer_t* handle) {
    auto self = (Timer*)handle->data;
    {
      auto delta = self->ref - high_resolution_clock::now();
      auto delta_count = duration_cast<milliseconds>(delta).count();
      is::log::info("tick... delta={}ms", delta_count);
    }

    { 
      self->ref = self->ref + milliseconds(self->period);
      auto delta = self->ref - high_resolution_clock::now();
      auto delta_count = duration_cast<milliseconds>(delta).count();
      uv_timer_start(&self->timer, self->callback, delta_count, 0);
    }
  }
};

Timer timer(uv_default_loop(), 1000);
uv_run(uv_default_loop(), UV_RUN_DEFAULT);

输出:

[info][14:20:19:772354] tick... delta=-1ms
[info][14:20:20:771120] tick... delta=0ms
[info][14:20:21:772235] tick... delta=-1ms
[info][14:20:22:771040] tick... delta=0ms
[info][14:20:23:772174] tick... delta=0ms
[info][14:20:24:771302] tick... delta=0ms
[info][14:20:25:771448] tick... delta=0ms
[info][14:20:26:771568] tick... delta=0ms
[info][14:20:27:771117] tick... delta=0ms
[info][14:20:28:772250] tick... delta=-1ms
[info][14:20:29:771374] tick... delta=0ms
[info][14:20:30:771495] tick... delta=0ms
[info][14:20:31:771608] tick... delta=0ms
[info][14:20:32:771691] tick... delta=0ms

【问题讨论】:

  • 不要指望计时器是准确的。甚至硬件也不太准确。

标签: c++ timer libuv drift


【解决方案1】:

libuv 重复计时器不会对其回调所用的时间进行调整。在调用回调之前,它们会在重复间隔期间重新参与,请参见此处:https://github.com/libuv/libuv/blob/v1.x/src/unix/timer.c#L165

有些人会同时考虑它,所以为了调整您的期望,我建议您使用单次计时器并在您认为合适的回调中重新设置它,就像您所做的那样。

事后看来,我希望我们不包括重复计时器。

【讨论】:

    【解决方案2】:

    它没有正确漂移。它有更长的(让我说)延迟可能是因为回调安排在t毫秒之后发生的第一次迭代。
    实际上,libev 也有类似的行为,如果您考虑到有时回调是在时间到期之前安排的(至少在您的示例中),那就更糟了。
    请注意,计时器不提供严格的时间表,而是尽力而为的方法(即,它会在间隔后尽快执行回调 - 作为一个不太好的示例,如果您有 100 个 CPU 限制会发生什么同时触发的进程?你不能指望所有的回调都没有延迟)。

    【讨论】:

    • 不确定我是否同意你的看法。如果我安排一个计时器每秒执行一次,那么在 1000 秒后,我希望它执行 1000 次。但是由于 libuv 的“漂移”大约为 1ms,它只会被执行 999 次。因此,在我看来,libev 计时器在这里是正确的,因为它会自我调整以解决“漂移”问题。最后,我希望回调有一个延迟(由于 cpu 绑定进程,就像你提到的那样),但是计时器应该总是尝试通过提前安排下一个滴答声来补偿每次迭代的延迟。
    • @RodolfoPicoreti 这是错误的期望,即使我理解它。无论如何,每次re-starts 本身,就好像您使用自己的起点启动了一个新的独立计时器(与前一个计时器共享一个数据结构)。因此,没有补偿的余地。我并不是说您的期望总体上是错误的,它根本不适用于特定情况。 ;-)
    • 感谢您的帮助。我最终实现了自己的计时器逻辑以获得我想要的行为(我将编辑我的问题并将其添加到那里)。
    猜你喜欢
    • 2019-10-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-05-27
    • 2019-10-05
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多