Выполнение цикла точно один раз в секунду

33

Я запускаю этот цикл, чтобы проверять и печатать некоторые вещи каждую секунду. Однако, поскольку вычисления занимают, возможно, несколько сотен миллисекунд, время печати иногда пропускается за секунду.

Есть ли способ написать такой цикл, чтобы я гарантированно получал распечатку каждую секунду? (При условии, конечно, что вычисления в цикле занимают меньше секунды :))

while true; do
  TIME=$(date +%H:%M:%S)
  # some calculations which take a few hundred milliseconds
  FOO=...
  BAR=...
  printf '%s  %s  %s\n' $TIME $FOO $BAR
  sleep 1
done
forthrin
источник
Возможно, полезно: unix.stackexchange.com/q/60767/117549
Джефф Шаллер
26
Обратите внимание, что « точно один раз в секунду» буквально невозможно в большинстве случаев, потому что вы (обычно) работаете в пользовательском пространстве поверх вытесняющего многозадачного ядра, которое будет планировать ваш код так, как считает нужным (так что вы не сможете восстановить контроль сразу после сна). заканчивается, например). Если вы не пишете код C, который вызывает sched(7)API (POSIX: смотрите <sched.h>и страницы, ссылки на которые есть), вы в принципе не можете иметь гарантии в реальном времени для этой формы.
Кевин
Просто чтобы подтвердить то, что сказал @Kevin, использование sleep () для получения любого точного хронометража обречено на провал, это гарантирует, по крайней мере, 1 секунду сна. Если вам действительно нужны точные тайминги, вам нужно посмотреть на системные часы (см. CLOCK_MONOTONIC) и вызвать события, основанные на времени с момента последнего события + 1 с, и убедиться, что вы не отключились, потратив на запуск> 1 с, вычисление в следующий раз после некоторой операции и т. д.
John U
только собирается оставить это здесь falsehoodsabouttime.com
Ало Malbarez
Точно один раз в секунду = используйте VCXO. Только программное решение поможет вам получить «достаточно хорошее», но не точное.
Ян Макдональд,

Ответы:

65

Чтобы остаться немного ближе к исходному коду, я делаю так:

while true; do
  sleep 1 &
  ...your stuff here...
  wait # for sleep
done

Это немного меняет семантику: если ваш материал занял меньше секунды, он просто будет ждать, пока пройдет полная секунда. Однако, если ваш материал по какой-либо причине занимает больше секунды, он не будет порождать даже больше подпроцессов, и это никогда не закончится.

Таким образом, ваши вещи никогда не работают параллельно, а не в фоновом режиме, поэтому переменные тоже работают как положено.

Обратите внимание, что если вы также запустите дополнительные фоновые задачи, вам придется изменить waitинструкцию, чтобы только ждать sleepконкретного процесса.

Если вам нужно, чтобы он был еще более точным, вам, скорее всего, придется просто синхронизировать его с системными часами и мс сна вместо полных секунд.


Как синхронизировать системные часы? Понятия не имею, глупая попытка

По умолчанию:

while sleep 1
do
    date +%N
done

Выход: 003511461 010510925 016081282 021643477 028504349 03 ... (продолжает расти)

синхронизированные:

 while sleep 0.$((1999999999 - 1$(date +%N)))
 do
     date +%N
 done

Выход: 002648691 001098397 002514348 001293023 001679137 00 ... (остается тем же)

frostschutz
источник
9
Этот трюк сна / ожидания действительно умный!
philfr
Мне интересно, все ли реализации sleepобрабатывают доли секунды?
jcaron
1
@jcaron не все из них. но это работает для сна gnu и сна busybox, так что это не экзотика. Вы, вероятно, могли бы сделать простой откат, например, sleep 0.9 || sleep 1неверный параметр - практически единственная причина, по которой сон никогда не срабатывает.
frostschutz
@frostschutz Я бы ожидал, что sleep 0.9его будут интерпретировать как sleep 0наивные реализации (учитывая, что это atoiбудет делать). Не уверен, что это приведет к ошибке.
jcaron
1
Я рад, что этот вопрос вызвал большой интерес. Ваше предложение и ответ очень хороши. Он не только держится за секунду, но и держится как можно ближе ко всей секунде. Впечатляет! (PS! gdatedate +%N
Напомним, что
30

Если вы можете перестроить свой цикл в сценарий / Oneliner то самый простой способ сделать это с watchи его preciseвариант.

Вы можете увидеть эффект с помощью watch -n 1 sleep 0.5- он покажет количество секунд, но иногда пропускает секунду. Запуск его as watch -n 1 -p sleep 0.5выдаст два раза в секунду, каждую секунду, и вы не увидите пропусков.

мальстрем
источник
11

Выполнение операций в подоболочке, выполняемой как фоновое задание, сделает их не так сильно мешающими sleep.

while true; do
  (
    TIME=$(date +%T)
    # some calculations which take a few hundred milliseconds
    FOO=...
    BAR=...
    printf '%s  %s  %s\n' "$TIME" "$FOO" "$BAR"
  ) &
  sleep 1
done

Единственное время, «украденное» из одной секунды, - это время, затрачиваемое на запуск подоболочки, поэтому в конечном итоге она пропускает секунду, но, надеюсь, реже, чем исходный код.

Если код в подоболочке заканчивается более чем на секунду, цикл начинает накапливать фоновые задания и в конечном итоге исчерпывает ресурсы.

Кусалананда
источник
9

Другой альтернативой (если вы не можете использовать, например, watch -pкак предлагает Maelstrom) является sleepenh[ manpage ], которая предназначена для этого.

Пример:

#!/bin/sh

t=$(sleepenh 0)
while true; do
        date +'sec=%s ns=%N'
        sleep 0.2
        t=$(sleepenh $t 1)
done

Обратите внимание на sleep 0.2то, что симулятор выполняет некоторое трудоемкое задание, съедая около 200 мс. Несмотря на это, выход наносекунд остается стабильным (ну, по стандартам ОС не в реальном времени) - это происходит раз в секунду:

sec=1533663406 ns=840039402
sec=1533663407 ns=840105387
sec=1533663408 ns=840380678
sec=1533663409 ns=840175397
sec=1533663410 ns=840132883
sec=1533663411 ns=840263150
sec=1533663412 ns=840246082
sec=1533663413 ns=840259567
sec=1533663414 ns=840066687

Это меньше 1 мс, и никаких тенденций. Это довольно хорошо; Вы должны ожидать отказов не менее 10 мс, если в системе есть какая-либо нагрузка, но со временем она не будет дрейфовать. Т.е. вы не потеряете ни секунды.

derobert
источник
7

С zsh:

n=0
typeset -F SECONDS=0
while true; do
  date '+%FT%T.%2N%z'
  ((++n > SECONDS)) && sleep $((n - SECONDS))
done

Если ваш сон не поддерживает с плавающей точкой секунд, вы можете использовать zsh«S zselectвместо (после zmodload zsh/zselect):

zmodload zsh/zselect
n=0
typeset -F SECONDS=0
while true; do
  date '+%FZ%T.%2N%z'
  ((++n > SECONDS)) && zselect -t $((((n - SECONDS) * 100) | 0))
done

Они не должны дрейфовать до тех пор, пока команды в цикле будут выполняться менее одной секунды.

Стефан Шазелас
источник
0

У меня было точно такое же требование для сценария оболочки POSIX, где все помощники (usleep, GNUsleep, sleepenh, ...) недоступны.

см .: https://stackoverflow.com/a/54494216

#!/bin/sh

get_up()
{
        read -r UP REST </proc/uptime
        export UP=${UP%.*}${UP#*.}
}

wait_till_1sec_is_full()
{
    while true; do
        get_up
        test $((UP-START)) -ge 100 && break
    done
}

while true; do
    get_up; START=$UP

    your_code

    wait_till_1sec_is_full
done
Бастиан Битторф
источник