Пытаясь реализовать простой таймер с использованием 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]: (разница всегда больше 1,0 с)
[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
Таймеры повторения libuv не изменяют время, необходимое для их обратного вызова. Они повторно включаются на время интервала повторения непосредственно перед вызовом обратного вызова, см. Здесь: https://github.com/libuv/libuv/blob/v1.x/src/unix/timer.c#L165
Есть люди, которые смотрят на это в обоих направлениях, поэтому, чтобы скорректировать свои ожидания, я предлагаю вам использовать таймер единого выстрела и перезапускать его в обратном вызове так, как вы считаете нужным, как и вы.
Оглядываясь назад, хотелось бы, чтобы мы не включали повторяющиеся таймеры.
Это не правильно дрейфует. Это дольше (позвольте мне сказать) задержка вероятно, из-за того, что обратный вызов запланирован на первой итерации, которая происходит после t
миллисекунды.
На самом деле, у libev похожее поведение, даже хуже, если учесть, что иногда обратный вызов запланирован до истечения времени (по крайней мере, в вашем примере).
Обратите внимание, что таймер предлагает не строгое расписание, а подход с максимальным усилием (то есть он выполняет обратный вызов как можно скорее после интервала — как не очень хороший пример, что происходит, если у вас ограничено 100 ЦП процессы, которые запускаются одновременно? Вы не можете ожидать, что все обратные вызовы вообще не будут задерживаться).