[Из песочницы] Периодическое обновление данных
Сразу хочу оговорится, что наш код выполняется в виртуальной среде (машине) 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;
}
}
}