Сразу хочу оговорится, что наш код выполняется в виртуальной среде(машине) .NET Framework которая в свою очередь исполняется на операционной системе общего назначения, поэтому говорить о какой либо точности даже в пределах 1-2 мс мы не будем. Но тем не менее попытаемся сделать все от зависящие, чтобы увеличить временную точность.
Зачастую в нашей программе, возникает необходимость обновление какой-либо информации c определенным временным интервалом. В моем случаи это было обновление снапшотов (изображений) с ip камер. Зачастую бизнес логика приложения устанавливает перед нами определенные ограничения частоты обновления данных. Для это время составляет 1 секунда.
Решение в лоб — это установить Thread.Sleep(1000)/Task.Await(1000) после запроса снапшота.
Но срок выполнения нашей операции — недетерминированная величина. Поэтому имитация взятия снапшота выглядит примерно так:
Запустим наше программу и запустим вывод
Как мы видим отставания будут накапливаться и следовательно бизнес логика нашего приложения нарушатся.
Можно попробовать сделать замер среднего отставания от и уменьшить время сна, уменьшив средние отклонение, но в этом случаи мы можем получить больше запросов чем требует наша бизнес логика. Мы некогда не сможем предугадать зная что запрос выполняется до 1 секунды — сколько милисекунд нам нужно подождать, чтобы обеспечить необходимый период обновления.
Напрашивается очевидное решение. Замерить время до выполнения операции и после. И рассчитать их разницу.
Запустим нашу программу теперь. Если Вам повезет вы увидите примерно следующие.
Почему я написал если повезет? Потому что watch.Start() выполняется до DoSomethink() и watch.Stop() после DoSomethink(); Эти операции не мгновенны + сама среда выполнения не гарантирует точность времени исполнения программы (x). Поэтому будут существовать накладные расходы. Наша функция DoSomethink() выполняется от 0-1000 мс (y). Следовательно могут возникнуть ситуации когда x + y > 1000 в таких случаях
будет принимать отрицательные значения и мы получить ArgumentOutOfRangeException так как метод Thread.Sleep() не должен принимать отрицательные значения.
В таких случаях имеет смысл установить время needSleepMs в 0;
На самом деле в реальности функция DoSomethink() может выполнятся сколь угодно долго и мы можем получить переполнение переменной при приведении к int. Тогда время нашего сна
может превысить sleepMs;
Можно исправить это следующим образом:
В принципе все готово. Но использование подобного подхода даже в 1 месте вызывает дискомфорт для глаза программиста. А если таких мест в программе десятки то код превратится в нечитабельную кучу…
Чтобы исправить эту ситуацию инкапсулируем наш код в функцию. Тут можно убрать в отдельный класс либо и использовать как обычный метод к класс помойку Global и использовать как статический(мой вариант).
В нашем примере оставим для простоты оставим его в классе Program
Наша функции на входе принимает ссылку на функцию которую необходимо выполнить и наше планируемое время ожидания. А возвращает время которое следует спать нашей программе.
Для удобства использования мы можем также передавать анонимные лямбда функции в нашу функцию.
Полный листинг программы приведен ниже:
Зачастую в нашей программе, возникает необходимость обновление какой-либо информации 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.Start() выполняется до 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 и использовать как статический(мой вариант).
В нашем примере оставим для простоты оставим его в классе Program
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(); before(); watch.Stop(); var needSleepMs = sleepMs - watch.ElapsedMilliseconds; return needSleepMs > 0 ? (int) needSleepMs : 0; } } }
