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


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

Можливо, корисно: unix.stackexchange.com/q/60767/117549
Jeff Schaller

26
Зауважте, що " точно раз на секунду" неможливо буквально в більшості випадків, тому що ви (як правило) працюєте в просторі користувачів на вершині попередньо багатозадачного ядра, яке планує ваш код так, як він вважає за потрібне (так що ви не зможете відновити контроль відразу після сну кінців, наприклад). Якщо ви не пишете код C, який дзвонить в sched(7)API (POSIX: див. <sched.h>Та пов’язані звідти сторінки), ви, як правило, не можете мати гарантії цієї форми в режимі реального часу.
Кевін

Тільки щоб створити резервну копію того, що сказав @Kevin, використовуючи сон (), щоб спробувати отримати будь-які точні терміни, приречений на провал, це гарантує лише ПОСЛУШ 1 сек сну. Якщо вам справді потрібні точні таймінги, вам потрібно подивитися на системний годинник (див. CLOCK_MONOTONIC) та запустити речі на основі часу з моменту останньої події + 1, і переконайтеся, що ви не відключите себе, взявши> 1 сек для запуску, обчислення наступного разу після деякої операції тощо
Джон У

просто збираюся залишити це тут falsehoodsabouttime.com
alo 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 ... (залишається таким же)


9
Ця хитрість сну / очікування дійсно розумна!
philfr

Мені цікаво, чи всі реалізації sleepобробляють дробові секунди?
jcaron

1
@jcaron не всі з них. але він працює для сну і засинання, тому це не екзотика. Можливо, ви могли б зробити простий запасний приклад, як, наприклад sleep 0.9 || sleep 1, невірний параметр - це єдина причина, коли сон не може вийти з ладу.
frostschutz

@frostschutz Я б очікував, sleep 0.9що його трактують як sleep 0наївні реалізації (враховуючи, що atoiце робиться). Не впевнений, чи це насправді призведе до помилки.
jcaron

1
Я радий, що це питання викликало чималий інтерес. Ваша пропозиція та відповідь дуже хороші. Він не тільки тримається протягом другого, він також прилипає якомога ближче до всієї секунди. Вражає! (PS! Зі сторони, потрібно встановити GNU Coreutils та використовувати gdateна macOS для date +%Nроботи.)
trrin

30

Якщо ви можете реструктурувати цикл у скрипт / oneliner, то найпростіший спосіб зробити це з допомогою watchі йогоprecise параметра.

Ви можете бачити ефект за допомогою watch -n 1 sleep 0.5- він показуватиме відлік секунди, але час від часу пропускатиметься за секунду. Якщо запустити його, 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 мс, якщо є будь-яке навантаження на систему - але все одно ніякого дрейфу з часом. Тобто, ти не втратиш ні секунди.


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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.