Как стать автором
Обновить

Комментарии 25

А чем таймеры не угодили?
Ну по вашей же ссылке упоминаются Multimedia Timers, про которые Microsoft пишет, что они «with the greatest resolution (or accuracy) possible for the hardware platform». На том же SO есть пример обвязки для C#.
Вместо этих p/invoke таймеров я бы бахнул свой тред с spin-wait (на промежутки меньше 3 мс) и Stopwatch и sleep(0) на большее время.
ага, на 2 секунды в сутки, а вам абсолютно точно нужно делать снимки раз в секунду что интервал в 1.000023 сек не пойдет?
Я не автор, но автор гнался за миллисекундами и «просто» обычный таймер с его дрейфом не подходит т.к. надо делать ручные коррекции.
Эммм… А что тогда делает приведённое решение? Его ж будет из стороны в сторону носить по полной программе.

PS: По теме статьи можно предположить, что интервал должен выдерживаться в среднем, что порождает несколько иное решение.
и запускать по таймеру асинхронный процесс который может выполняться сколько ему угодно. Главное поставить защиту от повторного входа чтобы в некоторых случаях не получить много параллельных тредов.

А если сделать лаг в одну секунду и предзагружать изображения? Получится вывод почти точно каждую секунду.

В этом фрагменте
    var needSleepMs = sleepMs - watch.ElapsedMilliseconds;
    if (needSleepMs > 0 && watch.ElapsedMilliseconds <= sleepMs) 
        return (int) needSleepMs;
    return 0;

можно убрать сравнение watch.ElapsedMilliseconds <= sleepMs, так как если watch.ElapsedMilliseconds будет больше sleepMs, то needSleepMs будет меньше нуля, что уже отсекается условием needSleepMs > 0.

По сути нам надо вернуть неотрицательное значение. Код
    var needSleepMs = sleepMs - watch.ElapsedMilliseconds;
    if (needSleepMs > 0) 
        return (int) needSleepMs;
    return 0;
делает именно это. А дальше его можно сократить до return needSleepMs > 0 ? (int)needSleepMs : 0;.

Если условие инвертировать, то желаемое поведение (получение неотрицательного значения) станет ещё более явным: return needSleepMs < 0 ? 0 : (int)needSleepMs;

И второй момент — Stopwatch.StartNew() возвращает уже запущенный экземпляр и делать watch.Start() не обязательно.
огромное спасибо за хороший совет. Это сравнение осталось с моего старого кода когда я приводил к инту до сравнения и могло возникнуть переполнение.
Math.Max тоже подойдет, именно такая логика же здесь?
Ох, вспомнилось как я синхронизировал видеопоток с аудиопотоком под Emscripten-ом на Сях… Только я когда отрицательную величину получал — я её сохранял и таки обнулял, чтоб в одной из следующих итераций вычесть из положительного значения. Ну и да, я использовал SDL_Delay — получалось точнее. Хотя по итогу всё равно дрифт получался, и пришлось синхронить по звуку…
На тасках это можно сделать немного проще
class Program
{
    static void Main(string[] args)
    {
        while(true)
        {
            Task.WaitAll(Task.Run(GetSnapshot), Task.Delay(1000));
        }
    }

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

Такой код накапливает ошибку постоянно (около 0.1 с каждые 10 с).
Да но как я уже говорил в самом начале идеальной точности достичь не удастся так как мы работаем на виртуальной машине и в операционной системе общего назначения а не реального времени.
Ваш код набегает на 0.01 с на 10 с. Хорошее решение!
Можно не накапливать ошибку «плывя» по времени. Для этого нужно заложиться на источник текущего времени. Правда тут много подводных камней — см. habr.com/ru/post/146109

Дело в том, что если решать задачу на разного рода sleep-ах / delay-ях — мы отвязаны от текущего времени и будем накапливать ошибку:
00:00.00
00:01.00
00:02.01
00:03.01
...
00:58.42
00:59.42
01:00.43
...
02:13.99
02:15.00 <--- 14.99 + накопленная погрешность
02:16.00
02:17.01


А если привязаться к реальному времени, то мы будем «болтаться» около реального значения с некоторой, естественной для не-RTOS операционки, погрешностью.
00:00.12
00:01.15
00:02.07
00:03.09
...
00:58.14
00:59.19
01:00.04
...
02:13.18
02:14.23
02:15.11
02:16.14


Так что погрешность погрешности — рознь.

Подход привязки к реальному времени я использовал в продакшене при реализации микширования звука нескольких голосовых звонков: звуковые потоки от нескольких абонентов плавают друг относительно друга, но они все подтягиваюся к реальному времени и взаимная погрешность не нарастает.
На самом деле не для всех задач это важно, лично я такой подход использую, когда мне нужно раз в 5 минут тянуть стянуть данные из какой-нибудь системы и отобразить на экране (Если лень тянуть шедулер типа кварца). Если как то заботится об этой накапливаемой ошибке, то придется заботиться еще что `GetSnapshot()`должен отрабатывать заведомо меньше чем `sleepMs` и так далее, в общем задача абсолютно другого уровня.
Спасибо за хороший пример.
Тоже когда-то страдал Thread.Sleep(1000)/Task.Await(1000), таймерами и т.д. А потом открыл Quartz.NET www.quartz-scheduler.net/documentation/quartz-3.x/quick-start.html и с тех пор его и использую, он для данной задачи избыточен конечно, но зато CRON умеет и если нужно что-то дергать каждый чт в 10:00 — идеален, ну а если каждую секунду то «0/1 * * * * ?»

Можно еше поковырять ThreadPool.RegisterWaitForSingleObject() — он как раз для таких случаев подходит и избавляет от возни с вычислением миллисекунд.


Ну а по коду вот что. Во-первых, в тех местах, где int используется в качестве "столько-то миллисекунд", замените его на человечий TimeSpan. Во-вторых, совсем правильно было бы написать метод, типа PerformPeriodicCallback(Action callback, TimeSpan interval, WaitHandle stopEvent).

А чем такой вариант плох?
int prev = -1, ms;
while (true)
{
    ms = DateTime.Now.Millisecond;
    if (ms % 1000 == 0 && prev == 999)
        Getsnapshot();             
    prev = ms;
}        
Бесполезной тратой процессорного времени.
А если добавить Thread.Sleep(900) и асинхронно запускать получение снэпшота?
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Изменить настройки темы

Истории