Lancer une boucle avec précision une fois par seconde

33

Je lance cette boucle pour vérifier et imprimer certaines choses à chaque seconde. Cependant, comme les calculs prennent peut-être quelques centaines de millisecondes, le temps imprimé saute parfois une seconde.

Existe-t-il un moyen d'écrire une telle boucle que je sois assuré d'obtenir une impression à chaque seconde? (À condition bien sûr que les calculs dans la boucle prennent moins d'une seconde :))

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
en avant
la source
Peut-être utile: unix.stackexchange.com/q/60767/117549
Jeff Schaller
26
Notez que " précisément une fois par seconde" n’est pas littéralement possible dans la plupart des cas car vous exécutez (généralement) en espace utilisateur sur un noyau multitâche préemptif qui planifiera votre code comme il le souhaite (de sorte que vous ne pourriez pas reprendre le contrôle immédiatement après une mise en veille. se termine, par exemple). Sauf si vous écrivez du code C qui appelle dans l' sched(7)API (POSIX: voir <sched.h>et les pages liées à partir de là), vous ne pouvez en principe pas avoir de garanties en temps réel de ce formulaire.
Kevin
Juste pour confirmer ce que @Kevin a dit, utiliser sleep () pour essayer d’obtenir un chronométrage précis est voué à l’échec, cela ne garantit qu’au moins 1 seconde de sommeil. Si vous avez vraiment besoin de chronomètres précis, vous devez examiner l'horloge système (voir CLOCK_MONOTONIC), déclencher des actions en fonction du temps écoulé depuis le dernier événement + 1 et ne pas vous tromper en prenant> 1 seconde pour courir. calcul de la prochaine fois après une opération, etc.
John U
Je
Précisément une fois par seconde = utilisez un VCXO. Une solution uniquement logicielle vous amènera seulement à "assez bon", mais pas précis.
Ian MacDonald

Réponses:

65

Pour rester un peu plus proche du code d'origine, ce que je fais est:

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

Cela change un peu la sémantique: si vos fichiers ont pris moins d'une seconde, ils attendront simplement que la seconde soit complète. Cependant, si votre travail prend plus d'une seconde pour une raison quelconque, il ne créera pas encore plus de sous-processus sans fin.

Ainsi, vos données ne sont jamais exécutées en parallèle et non en arrière-plan, de sorte que les variables fonctionnent également comme prévu.

Notez que si vous démarrez également des tâches d'arrière-plan supplémentaires, vous devez modifier l' waitinstruction pour n'attendre que le sleepprocessus en particulier.

Si vous avez besoin d’être encore plus précis, vous devrez probablement le synchroniser avec l’horloge système et le mode veille au lieu de quelques secondes.


Comment synchroniser avec l'horloge système? Aucune idée vraiment, tentative stupide:

Défaut:

while sleep 1
do
    date +%N
done

Sortie: 003511461 010510925 016081282 021643477 028504349 03 ... (continue de croître)

Synced:

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

Sortie: 002648691 001098397 002514348 001293023 001679137 00 ... (reste identique)

Frostschutz
la source
9
Cette astuce sommeil / attente est vraiment intelligente!
philfr
Je me demande si toutes les implémentations de sleepmanipuler des fractions de secondes?
Jcaron
1
@jcaron pas tous. mais cela fonctionne pour gnu sleep et busybox sleep alors ce n’est pas exotique. Vous pouvez probablement faire un simple repli, sleep 0.9 || sleep 1car un paramètre invalide est à peu près la seule raison pour laquelle le sommeil échoue.
Frostschutz
@frostschutz Je m'attendrais sleep 0.9à être interprété comme une sleep 0implémentation naïve (étant donné que c'est ce que atoinous ferions). Je ne sais pas si cela entraînerait une erreur.
jcaron
1
Je suis heureux de voir que cette question a suscité beaucoup d'intérêt. Votre suggestion et votre réponse sont très bonnes. Non seulement il reste dans la seconde, mais il reste aussi proche que possible de la seconde entière. Impressionnant! (PS! Sur une note de côté, il faut installer GNU Coreutils et l’utiliser gdatesur macOS pour faire date +%Nfonctionner.)
jeudi
30

Si vous pouvez restructurer votre boucle dans un script / oneliner puis façon de faire la plus simple est avec watchet son preciseoption.

Vous pouvez voir l’effet avec watch -n 1 sleep 0.5- il indiquera le nombre de secondes comptées, mais sautera parfois une seconde. Courir comme watch -n 1 -p sleep 0.5volonté sortie deux fois par seconde, chaque seconde, et vous ne verrez pas skips.

Maelstrom
la source
11

L'exécution des opérations dans un sous-shell fonctionnant en tâche de fond les empêcherait d'interférer autant avec le 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

Le seul temps "volé" de la seconde serait le temps nécessaire au lancement du sous-shell, de sorte qu'il sera éventuellement sauté une seconde, mais espérons-le moins souvent que le code d'origine.

Si le code dans le sous-shell utilise plus d'une seconde, la boucle commence à accumuler des travaux en arrière-plan et finit par manquer de ressources.

Kusalananda
la source
9

Une autre alternative (si vous ne pouvez pas utiliser, par exemple, watch -pcomme le suggère Maelstrom) est sleepenh[ page de manuel ], qui est conçue pour cela.

Exemple:

#!/bin/sh

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

Notez sleep 0.2que la simulation simule une tâche fastidieuse qui consomme environ 200 ms. Malgré cela, la sortie en nanosecondes reste stable (enfin, par rapport aux normes des systèmes d’exploitation non temps réel) - cela se produit une fois par seconde:

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

C'est sous 1ms différent, et pas de tendance. C'est très bien; vous devez vous attendre à des rebonds d'au moins 10 ms s'il y a une charge sur le système, mais pas de dérive dans le temps. C'est-à-dire que vous ne perdrez pas une seconde.

derobert
la source
7

Avec zsh:

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

Si votre sommeil ne prend pas en charge les secondes en virgule flottante, vous pouvez utiliser celui zshde zselect(après a 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

Celles-ci ne doivent pas dériver tant que les commandes de la boucle prennent moins d'une seconde à s'exécuter.

Stéphane Chazelas
la source
0

J'avais exactement la même exigence pour un script shell POSIX, où toutes les aides (usleep, GNUsleep, sleepenh, ...) ne sont pas disponibles.

voir: 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
la source