Uruchamianie pętli dokładnie raz na sekundę


33

Korzystam z tej pętli, aby sprawdzać i drukować niektóre rzeczy co sekundę. Ponieważ jednak obliczenia mogą potrwać kilkaset milisekund, wydrukowany czas czasami przeskakuje o sekundę.

Czy jest jakiś sposób na napisanie takiej pętli, że mam gwarancję otrzymania wydruku co sekundę? (Oczywiście pod warunkiem, że obliczenia w pętli zajmą mniej niż sekundę :))

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


26
Zauważ, że „ dokładnie raz na sekundę” nie jest dosłownie możliwe w większości przypadków, ponieważ (zwykle) pracujesz w przestrzeni użytkownika na zapobiegawczym wielozadaniowym jądrze, które zaplanuje twój kod według własnego uznania (abyś nie mógł odzyskać kontroli natychmiast po śnie kończy się na przykład). O ile nie piszesz kodu C, który wywołuje sched(7)interfejs API (POSIX: patrz <sched.h>i strony z niego połączone), zasadniczo nie możesz mieć gwarancji tego formularza w czasie rzeczywistym.
Kevin,

Wystarczy, że utworzę kopię zapasową tego, co powiedział @Kevin, używając funkcji sleep () w celu uzyskania precyzyjnego pomiaru czasu jest skazana na niepowodzenie, gwarantuje to TYLKO 1 sekundę snu. Jeśli naprawdę potrzebujesz dokładnych czasów, musisz spojrzeć na zegar systemowy (patrz CLOCK_MONOTONIC) i wyzwalać rzeczy w oparciu o czas od ostatniego zdarzenia + 1s, i upewnij się, że nie potkniesz się, wykonując> 1 sekundę na uruchomienie, obliczanie następnego razu po jakiejś operacji itp.
John U


Dokładnie raz na sekundę = użyj VCXO. Rozwiązanie oparte tylko na oprogramowaniu zapewni Ci „wystarczająco dobre”, ale nie precyzyjne.
Ian MacDonald

Odpowiedzi:


65

Aby pozostać trochę bliżej oryginalnego kodu, robię to:

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

To trochę zmienia semantykę: jeśli twoje rzeczy zajęły mniej niż sekundę, po prostu poczeka na upłynięcie pełnej sekundy. Jednakże, jeśli twój materiał z jakiegoś powodu trwa dłużej niż sekundę, nie będzie generował jeszcze więcej podprocesów, bez końca.

Twoje rzeczy nigdy nie działają równolegle, a nie w tle, więc zmienne działają również zgodnie z oczekiwaniami.

Pamiętaj, że jeśli rozpoczniesz dodatkowe zadania w tle, będziesz musiał zmienić waitinstrukcję, aby czekać tylko na sleepkonkretny proces.

Jeśli potrzebujesz, aby był jeszcze dokładniejszy, prawdopodobnie będziesz musiał po prostu zsynchronizować go z zegarem systemowym i uśpieniem ms zamiast pełnych sekund.


Jak zsynchronizować z zegarem systemowym? Naprawdę nie mam pojęcia, głupia próba:

Domyślna:

while sleep 1
do
    date +%N
done

Wyjście: 003511461 010510925 016081282 021643477 028504349 03 ... (ciągle rośnie)

Zsynchronizowany:

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

Wyjście: 002648691 001098397 002514348 001293023 001679137 00 ... (pozostaje taki sam)


9
Ta sztuczka polegająca na uśpieniu / czekaniu jest naprawdę sprytna!
philfr

Zastanawiam się, czy wszystkie implementacje sleepobsługują ułamkowe sekundy?
jcaron

1
@jcaron nie wszystkie z nich. ale działa w trybie uśpienia GNU i zajęty, więc nie jest to egzotyczne. Prawdopodobnie możesz wykonać prosty powrót, jak sleep 0.9 || sleep 1niepoprawny parametr, jest właściwie jedynym powodem, dla którego sen nigdy się nie kończy.
frostschutz

@frostschutz Oczekiwałbym, że będę sleep 0.9interpretowany jako sleep 0naiwne implementacje (biorąc pod uwagę, że tak atoiby się stało ). Nie jestem pewien, czy to rzeczywiście spowodowałoby błąd.
jcaron

1
Cieszę się, że to pytanie wzbudziło duże zainteresowanie. Twoje sugestie i odpowiedzi są bardzo dobre. Nie tylko utrzymuje się w ciągu sekundy, ale także trzyma się jak najbliżej całej sekundy, jak to możliwe. Imponujący! (PS! Na marginesie, należy zainstalować GNU Coreutils i używać go gdatew systemie macOS, aby date +%Ndziałało).
Dalej

30

Jeśli możesz zrestrukturyzować swoją pętlę do skryptu / onelinera, najprostszym sposobem na to jest watchi jej preciseopcja.

Możesz zobaczyć efekt za pomocą watch -n 1 sleep 0.5- pokaże zliczanie sekund, ale czasami przeskakuje przez sekundę. Uruchomienie go tak, jak watch -n 1 -p sleep 0.5będzie generowane dwa razy na sekundę, co sekundę, i nie zobaczysz żadnych pominięć.


11

Uruchamianie operacji w podpowłoce, która działa jako zadanie w tle, sprawiłoby, że nie kolidowałyby tak bardzo z 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

Jedynym momentem, gdy „skradziono” z jednej sekundy, byłby czas potrzebny na uruchomienie podpowłoki, więc w końcu pominie sekundę, ale miejmy nadzieję, że rzadziej niż oryginalny kod.

Jeśli kod w podpowłoce zajmie więcej niż sekundę, pętla zacznie gromadzić zadania w tle i ostatecznie zabraknie zasobów.


9

Inną alternatywą (jeśli nie możesz użyć, np. watch -pJak sugeruje Maelstrom), jest sleepenh[ manpage ], który jest do tego przeznaczony.

Przykład:

#!/bin/sh

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

Zwróć uwagę na sleep 0.2to, że tam symulujesz wykonywanie czasochłonnego zadania, jedząc około 200 ms. Mimo to, wyjście nanosekund pozostaje stabilne (cóż, według nieoperacyjnych standardów systemu operacyjnego) - dzieje się to raz na sekundę:

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

To mniej niż 1 ms inaczej i bez trendu. To całkiem dobrze; powinieneś spodziewać się odbić co najmniej 10 ms, jeśli w systemie jest jakieś obciążenie - ale nadal nie ma dryfu w czasie. Tj. Nie stracisz ani sekundy.


7

Z zsh:

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

Jeśli sen nie obsługuje zmiennoprzecinkowych sekund, można użyć zsh„s zselectzamiast (po 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

Nie powinny one dryfować, dopóki wykonywanie poleceń w pętli zajmuje mniej niż sekundę.


0

Miałem dokładnie takie same wymagania dla skryptu powłoki POSIX, w którym wszyscy pomocnicy (usleep, GNUsleep, sleepenh, ...) nie są dostępni.

patrz: 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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.