Отображение текущего времени - вызов делегата точно 1 раз в секунду
В сети есть множество решений, как показывать текущее время в приложении. Есть решения и с таймерами, и асинхронные.
Но у всех решений, которые я находил есть одна проблема - они вызывают обновление интерфейса не точно раз в секунду.
- Пропуск секунд. Для тех, кто устанавливает интервал обновления на 1000 мс, вызов происходит реже, так как таймеры отчитывают секунду от окончания выполнения предыдущего обработчика до следующего. А не вызвают его ровно раз в секунду. И при попадании на границу смены значения секунд, часы подлагивают, пропускают секунду, например
10:33:12 => 10:33:14. - Поллинг. Есть решения похитрее, они обновляют время на экране не раз в секунду, а например 5 или даже больше раз в секунду. Чем чаще обновление - тем актуальнее время на экране. При обновлении от 10 раз в секунду и выше лаг получается максимум в плюс-минус интервал обновления, то есть при 10 обновлениях в секунду, интервал между сменой секунд будет от 0,9 до 1,1 секунды, что в принципе практически незаметно глазу. Но здесь другой минус - частые обновления интерфейса + частые запросы системного времени дают лишнюю нагрузку на систему.
Вроде задача с виду простая, но готового решения я не нашел, ну или плохо искал. И решил написать его сам.
Ответы (1 шт):
Требования при разработке решения были следующими:
- Кросплатформенность, не зависеть от конкретного окружения или типа приложения.
- Реализовать так просто, насколько это возможно.
Вот такое решение получилось:
public class ClockTimer
{
private readonly Action<DateTime> _action;
private CancellationTokenSource _cts;
public ClockTimer(Action<DateTime> action)
=> _action = action;
public async void Start()
{
if (_cts != null)
return;
try
{
using (_cts = new CancellationTokenSource())
{
while (true)
{
DateTime date = DateTime.Now;
_action(date);
await Task.Delay(1000 - date.Millisecond, _cts.Token);
}
}
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
Debug.Fail(ex.ToString());
}
_cts = null;
}
public void Stop()
=> _cts?.Cancel();
}
Здесь убиваю 2 зайцев: и точно раз в секунду вызываю обновление, при чем обновление происходит в сразу через .001-.020мс после смены секунд на часах, и сразу передаю уже полученную из системы дату в делегат. Фактическая девиация генерируется только расходами на асинхронность.
Я проверил визуально в WPF вот таким образом
ClockTimer clock = new ClockTimer(d => Text = d.ToString("HH:mm:ss.fff"));
clock.Start();
Где Text - свойство, к которому привязан TextBlock, и сравнил с обновлением системных часов Windows в трее. Смена минут происходит либо одновременно, либо в приложении даже раньше.
Для наглядности работы, испытал и в консоли
static void Main(string[] args)
{
ClockTimer clock = new ClockTimer(d => Console.WriteLine(d.ToString("HH:mm:ss.fff")));
clock.Start();
Console.ReadKey();
}
Вывод консоль
02:10:25.431
02:10:26.077
02:10:27.019
02:10:28.009
02:10:29.011
02:10:30.005
02:10:31.021
02:10:32.012
02:10:33.011
02:10:34.017
02:10:35.009
02:10:36.016
02:10:37.014
02:10:38.009
Как видно, сразу после запуска происходит стабилизация, и затем вызов ровно раз в секунду максимально близко в смене секунд на часах.
При лагах в системе, перенагрузке на процессор или подвисаниях, делегат вызовется сразу как только будет возможно, и далее снова стабилизируется как при первичном запуске, и это отвечает условиям. Даже если в сам делегат воткнуть например Thread.Sleep(500), подвешивая поток, вызов все равно будет ровно 1 раз в секунду, хоть и с опозданием на те самые 500мс от момента смены секунд на часах.