[Из песочницы] Периодическое обновление данных

?v=1

Сразу хочу оговорится, что наш код выполняется в виртуальной среде (машине) Entity Framework которая в свою очередь исполняется на операционной системе общего назначения, поэтому говорить о какой либо точности даже в пределах 1–2 мс мы не будем. Но тем не менее попытаемся сделать все от зависящие, чтобы увеличить временную точность.

Зачастую в нашей программе, возникает необходимость обновление какой-либо информации c определенным временным интервалом. В моем случаи это было обновление снапшотов (изображений) с ip камер. Зачастую бизнес логика приложения устанавливает перед нами определенные ограничения частоты обновления данных. Для это время составляет 1 секунда.
Решение в лоб — это установить Thread.Sleep (1000)/Task.Await (1000) после запроса снапшота.

static void Getsnapshot()
{
  var rnd = new Random()
  var sleepMs = rnd.Next(0, 1000);
  Console.WriteLine($"[{DateTime.Now.ToString("mm:ss.ff")}] DoSomethink {sleepMs} ms");
  Thread.Sleep(sleepMs);
}

while (true)
{
  Getsnapshot();
  Thread.Sleep(1000);
}


Но срок выполнения нашей операции — недетерминированная величина. Поэтому имитация взятия снапшота выглядит примерно так:
Запустим наше программу и запустим вывод

[15:10.39] DoSomethink 974 ms
[15:12.39] DoSomethink 383 ms
[15:13.78] DoSomethink 99 ms
[15:14.88] DoSomethink 454 ms
[15:16.33] DoSomethink 315 ms
[15:17.65] DoSomethink 498 ms
[15:19.15] DoSomethink 708 ms
[15:20.86] DoSomethink 64 ms
[15:21.92] DoSomethink 776 ms
[15:23.70] DoSomethink 762 ms
[15:25.46] DoSomethink 123 ms
[15:26.59] DoSomethink 36 ms
[15:27.62] DoSomethink 650 ms
[15:29.28] DoSomethink 510 ms
[15:30.79] DoSomethink 257 ms
[15:32.04] DoSomethink 602 ms
[15:33.65] DoSomethink 542 ms
[15:35.19] DoSomethink 286 ms
[15:36.48] DoSomethink 673 ms
[15:38.16] DoSomethink 749 ms


Как мы видим отставания будут накапливаться и следовательно бизнес логика нашего приложения нарушатся.

Например, нам нужно получить массив из 60 изображений за 1 минуту а мы получим только 49.


Можно попробовать сделать замер среднего отставания от и уменьшить время сна, уменьшив средние отклонение, но в этом случаи мы можем получить больше запросов чем требует наша бизнес логика. Мы некогда не сможем предугадать зная что запрос выполняется до 1 секунды — сколько милисекунд нам нужно подождать, чтобы обеспечить необходимый период обновления.

Например, нам нужно получить массив из 60 изображений за 1 минуту а мы получим 62.


Напрашивается очевидное решение. Замерить время до выполнения операции и после. И рассчитать их разницу.

while (true)
{
   int sleepMs = 1000; 
   var watch = Stopwatch.StartNew();
   watch.Start();
   Getsnapshot();
   watch.Stop();
   int needSleepMs = (int)(sleepMs - watch.ElapsedMilliseconds);
   Thread.Sleep(needSleepMs);
}


Запустим нашу программу теперь. Если Вам повезет вы увидите примерно следующие.

[16:57.25] DoSomethink 789 ms
[16:58.05] Need sleep 192 ms

[16:58.25] DoSomethink 436 ms
[16:58.68] Need sleep 564 ms

[16:59.25] DoSomethink 810 ms
[17:00.06] Need sleep 190 ms

[17:00.25] DoSomethink 302 ms
[17:00.55] Need sleep 697 ms

[17:01.25] DoSomethink 819 ms
[17:02.07] Need sleep 181 ms

[17:02.25] DoSomethink 872 ms
[17:03.13] Need sleep 128 ms

[17:03.25] DoSomethink 902 ms
[17:04.16] Need sleep 98 ms

[17:04.26] DoSomethink 717 ms
[17:04.97] Need sleep 282 ms

[17:05.26] DoSomethink 14 ms
[17:05.27] Need sleep 985 ms


Почему я написал если повезет? Потому что watch.Star () выполняется до DoSomethink () и watch.Stop () после DoSomethink (); Эти операции не мгновенны + сама среда выполнения не гарантирует точность времени исполнения программы (x). Поэтому будут существовать накладные расходы. Наша функция DoSomethink () выполняется от 0–1000 мс (y). Следовательно могут возникнуть ситуации когда x + y > 1000 в таких случаях

 int needSleepMs = (int)(sleepMs - watch.ElapsedMilliseconds);


будет принимать отрицательные значения и мы получить ArgumentOutOfRangeException так как метод Thread.Sleep () не должен принимать отрицательные значения.

В таких случаях имеет смысл установить время needSleepMs в 0;
На самом деле в реальности функция DoSomethink () может выполнятся сколь угодно долго и мы можем получить переполнение переменной при приведении к int. Тогда время нашего сна
может превысить sleepMs;

Можно исправить это следующим образом:

var needSleepMs = sleepMs - watch.ElapsedMilliseconds;
if (needSleepMs > 0 && watch.ElapsedMilliseconds <= sleepMs)
{
   needSleepMs = (int)needSleepMs;
}
else
{
  needSleepMs = 0; 
}
Thread.Sleep(needSleepMs);


В принципе все готово. Но использование подобного подхода даже в 1 месте вызывает дискомфорт для глаза программиста. А если таких мест в программе десятки то код превратится в нечитабельную кучу…

Чтобы исправить эту ситуацию инкапсулируем наш код в функцию. Тут можно убрать в отдельный класс либо и использовать как обычный метод к класс помойку Global и использовать как статический (мой вариант).

В нашем примере оставим для простоты оставим его в классе Programm

public static int NeedWaitMs(Action before, int sleepMs)
{
  var watch = Stopwatch.StartNew();
  watch.Start();
  before();
  watch.Stop();
  var needSleepMs = sleepMs - watch.ElapsedMilliseconds;
  if (needSleepMs > 0 && watch.ElapsedMilliseconds <= sleepMs) 
    return (int) needSleepMs;
  return 0;
 }


Наша функции на входе принимает ссылку на функцию которую необходимо выполнить и наше планируемое время ожидания. А возвращает время которое следует спать нашей программе.
Для удобства использования мы можем также передавать анонимные лямбда функции в нашу функцию.

Полный листинг программы приведен ниже:

using System;
using System.Diagnostics;
using System.Threading;

namespace ConsoleApp2
{
    class Program
    {
        static void Getsnapshot()
        {
            var rnd = new Random();
            var sleepMs = rnd.Next(0, 1000);
            Console.WriteLine($"[{DateTime.Now.ToString("mm:ss.ff")}] DoSomethink {sleepMs} ms");
            Thread.Sleep(sleepMs);
        }

        static void Main(string[] args)
        {
            while (true)
            {
                var sleepMs = NeedWaitMs(Getsnapshot, 1000);
                Console.WriteLine($"[{DateTime.Now.ToString("mm:ss.ff")}] Need sleep {sleepMs} ms {Environment.NewLine}");
                Thread.Sleep(sleepMs);
            }
        }

        public static int NeedWaitMs(Action before, int sleepMs)
        {
            var watch = Stopwatch.StartNew();
            watch.Start();
            before();
            watch.Stop();
            var needSleepMs = sleepMs - watch.ElapsedMilliseconds;
            if (needSleepMs > 0 && watch.ElapsedMilliseconds <= sleepMs) 
                return (int) needSleepMs;
            return 0;
        }
    }
}

© Habrahabr.ru