web-dev-qa-db-fra.com

Exécution d'une boucle avec précision une fois par seconde

J'exécute 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 suis 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
33
forthrin

Pour rester un peu plus près du code d'origine, ce que je fais c'est:

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

Cela change un peu la sémantique: si votre truc a pris moins d'une seconde, il attendra simplement que la seconde entière passe. Cependant, si votre contenu prend plus d'une seconde pour une raison quelconque, il ne continuera pas à engendrer encore plus de sous-processus sans fin.

Donc, vos trucs ne tournent jamais en parallèle, et pas en arrière-plan, donc les variables fonctionnent aussi comme prévu.

Notez que si vous démarrez également des tâches d'arrière-plan supplémentaires, vous devrez modifier l'instruction wait pour n'attendre que le processus sleep spécifiquement.

Si vous en avez besoin pour être encore plus précis, vous n'aurez probablement qu'à le synchroniser avec l'horloge système et à dormir ms au lieu de secondes complètes.


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)

Synchronisé:

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

Sortie: 002648691 001098397 002514348 001293023 001679137 00 ... (reste la même)

65
frostschutz

Si vous pouvez restructurer votre boucle en un script/oneliner, la façon la plus simple de le faire est d'utiliser watch et son option precise.

Vous pouvez voir l'effet avec watch -n 1 sleep 0.5 - il affichera le décompte des secondes, mais sautera parfois une seconde. L'exécuter en tant que watch -n 1 -p sleep 0.5 sortira deux fois par seconde, toutes les secondes, et vous ne verrez aucun saut.

30
Maelstrom

L'exécution des opérations dans un sous-shell qui s'exécute comme un travail d'arrière-plan ne les gênerait pas 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é" à la seconde serait le temps pris pour lancer le sous-shell, donc il finirait par sauter une seconde, mais, espérons-le, moins souvent que le code d'origine.

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

11
Kusalananda

Une autre alternative (si vous ne pouvez pas utiliser, par exemple, watch -p comme le suggère Maelstrom) est sleepenh [ manpage ], qui est conçu pour cela.

Exemple:

#!/bin/sh

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

Noter la sleep 0.2 là-dedans, simuler une tâche longue et consommant environ 200 ms. Malgré cela, la sortie en nanosecondes reste stable (enfin, selon les normes de système d'exploitation non en 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 moins de 1 ms différent, et aucune tendance. C'est assez bien; vous devez vous attendre à des rebonds d'au moins 10 ms s'il y a une charge sur le système - mais toujours pas de dérive dans le temps. C'est-à-dire que vous ne perdrez pas une seconde.

9
derobert

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 à virgule flottante, vous pouvez utiliser zshzselect à la place (après un 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

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

7
Stéphane Chazelas

J'avais exactement la même exigence pour un script Shell POSIX, où tous les assistants (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
0
Bastian Bittorf