Führen Sie eine Schleife genau einmal pro Sekunde aus

33

Ich führe diese Schleife aus, um einige Dinge jede Sekunde zu überprüfen und auszudrucken. Da die Berechnungen jedoch einige Hundert Millisekunden dauern, überspringt die Druckzeit manchmal eine Sekunde.

Gibt es eine Möglichkeit, eine solche Schleife zu schreiben, bei der ich garantiert jede Sekunde einen Ausdruck bekomme? (Vorausgesetzt natürlich, dass die Berechnungen in der Schleife weniger als eine Sekunde dauern :))

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
quelle
Möglicherweise hilfreich: unix.stackexchange.com/q/60767/117549
Jeff Schaller
26
Beachten Sie, dass " genau ein Mal pro Sekunde" in den meisten Fällen buchstäblich nicht möglich ist, da Sie (normalerweise) im Benutzerbereich auf einem präventiven Multitasking-Kernel laufen, der Ihren Code nach Belieben plant (sodass Sie möglicherweise nicht sofort nach einem Ruhezustand die Kontrolle wiedererlangen endet zum Beispiel). Solange Sie keinen C-Code schreiben, der die sched(7)API aufruft (POSIX: siehe <sched.h>und von dort verknüpfte Seiten), können Sie im Grunde keine Echtzeitgarantien dieser Form haben.
Kevin
Nur um das zu sichern, was @Kevin gesagt hat, und um zu versuchen, mit sleep () ein präzises Timing zu erreichen, ist es zum Scheitern verurteilt, es garantiert nur mindestens 1 Sekunde Schlaf. Wenn Sie wirklich genaue Timings benötigen, müssen Sie auf die Systemuhr schauen (siehe CLOCK_MONOTONIC) und Dinge auslösen, die auf der Zeit seit dem letzten Ereignis + 1s basieren. Berechnung der nächsten Zeit nach einer Operation usw.
John U
gerade diese gehen lassen hier falsehoodsabouttime.com
alo Malbarez
Genau einmal in der Sekunde = Verwenden Sie einen VCXO. Eine reine Software-Lösung bringt Sie nur auf "gut genug", aber nicht präzise.
Ian MacDonald

Antworten:

65

Um dem ursprünglichen Code ein wenig näher zu kommen, mache ich Folgendes:

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

Dies ändert die Semantik ein wenig: Wenn Ihre Arbeit weniger als eine Sekunde gedauert hat, wartet sie einfach, bis die volle Sekunde verstrichen ist. Wenn Ihre Arbeit jedoch aus irgendeinem Grund länger als eine Sekunde dauert, werden nicht mehr Unterprozesse erzeugt, ohne dass dies zu Ende geht.

Ihre Inhalte werden also niemals parallel und nicht im Hintergrund ausgeführt, sodass auch Variablen wie erwartet funktionieren.

Beachten Sie, dass Sie, wenn Sie zusätzliche Hintergrundaufgaben starten, die waitAnweisung ändern müssen, um nur auf den sleepProzess zu warten .

Wenn Sie es noch genauer benötigen, müssen Sie es wahrscheinlich nur mit der Systemuhr synchronisieren und ms anstelle von vollen Sekunden in den Energiesparmodus versetzen.


Wie synchronisiere ich mit der Systemuhr? Keine Ahnung wirklich, blöder Versuch:

Standard:

while sleep 1
do
    date +%N
done

Ausgabe: 003511461 010510925 016081282 021643477 028504349 03 ... (wächst weiter)

Synchronisiert:

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

Ausgabe: 002648691 001098397 002514348 001293023 001679137 00 ... (bleibt gleich)

Frostschutz
quelle
9
Dieser Schlaf / Warte-Trick ist wirklich clever!
Philfr
Ich frage mich, ob alle Implementierungen von sleepSekundenbruchteilen behandeln?
Jcaron
1
@ jcaron nicht alle von ihnen. aber es funktioniert für Gnu-Schlaf und Busybox-Schlaf, also ist es nicht exotisch. Sie könnten wahrscheinlich einen einfachen Fallback durchführen, sleep 0.9 || sleep 1da ein ungültiger Parameter so ziemlich der einzige Grund dafür ist, dass der Schlaf jemals versagt.
Frostschutz
@frostschutz Ich würde erwarten sleep 0.9, sleep 0von naiven Implementierungen interpretiert zu werden ( vorausgesetzt , das atoiwürde reichen). Ich bin mir nicht sicher, ob dies tatsächlich zu einem Fehler führen würde.
Jcaron
1
Ich freue mich, dass diese Frage großes Interesse geweckt hat. Ihr Vorschlag und Ihre Antwort sind sehr gut. Es bleibt nicht nur in der Sekunde, es bleibt auch so nah wie möglich an der ganzen Sekunde. Beeindruckend! (PS! Nebenbei bemerkt, man muss GNU Coreutils installieren und gdateunter macOS verwenden, damit es date +%Nfunktioniert.)
Forthrin
30

Wenn Sie Ihre Schleife in ein Skript / einen Oneliner umstrukturieren können, ist dies mit watchund seiner preciseOption am einfachsten .

Sie können den Effekt mit watch -n 1 sleep 0.5- sehen, es werden Sekunden gezählt, aber gelegentlich wird eine Sekunde übersprungen. Wenn Sie es so ausführen, watch -n 1 -p sleep 0.5wird es zweimal pro Sekunde ausgegeben, und es werden keine Sprünge angezeigt.

Sog
quelle
11

Das Ausführen der Vorgänge in einer Subshell, die als Hintergrundjob ausgeführt wird, würde dazu führen, dass die Vorgänge nicht so stark beeinträchtigt werden 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

Die einzige Zeit, die von einer Sekunde an "gestohlen" wurde, war die Zeit, die zum Starten der Subshell benötigt wurde, sodass letztendlich eine Sekunde übersprungen wurde, aber hoffentlich seltener als der ursprüngliche Code.

Wenn der Code in der Subshell mehr als eine Sekunde benötigt, sammelt die Schleife Hintergrundjobs an und schließlich gehen die Ressourcen aus.

Kusalananda
quelle
9

Eine andere Alternative (wenn Sie sie zB nicht verwenden können, watch -pwie von Maelstrom vorgeschlagen) ist sleepenh[ manpage ], die für diesen Zweck entwickelt wurde.

Beispiel:

#!/bin/sh

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

Beachten Sie, sleep 0.2dass die Simulation einige zeitaufwändige Aufgaben ausführt, die etwa 200 ms dauern. Trotzdem bleibt die Nanosekunden-Ausgabe stabil (nach Nicht-Echtzeit-OS-Standards) - dies geschieht einmal pro Sekunde:

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

Das ist unter 1ms anders und kein Trend. Das ist ganz gut Sie sollten mit einem Sprung von mindestens 10 ms rechnen, wenn das System belastet ist - aber mit der Zeit immer noch keine Abweichung. Dh du wirst keine Sekunde verlieren.

derobert
quelle
7

Mit zsh:

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

Wenn Ihr Schlaf nicht unterstützt Punkt Sekunden schwimmen, können Sie verwenden zsh‚s zselectstatt (nach 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

Diese sollten nicht driften, solange die Befehle in der Schleife weniger als eine Sekunde dauern.

Stéphane Chazelas
quelle
0

Ich hatte genau die gleiche Anforderung an ein POSIX-Shell-Skript, bei dem nicht alle Helfer (usleep, GNUsleep, sleepenh, ...) verfügbar sind.

Siehe: 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
Bastian Bittorf
quelle