Esecuzione di un loop esattamente una volta al secondo


33

Sto eseguendo questo ciclo per controllare e stampare alcune cose ogni secondo. Tuttavia, poiché i calcoli richiedono forse alcune centinaia di millisecondi, il tempo stampato a volte salta un secondo.

Esiste un modo per scrivere un ciclo tale che mi viene garantito di ottenere una stampa ogni secondo? (A condizione, ovviamente, che i calcoli nel ciclo richiedano meno di un secondo :))

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
Nota che " precisamente una volta al secondo" non è letteralmente possibile nella maggior parte dei casi perché (di solito) stai eseguendo nello spazio utente su un kernel multitasking preventivamente che pianificherà il tuo codice come meglio crede (in modo da non poter riprendere il controllo immediatamente dopo uno sleep finisce, per esempio). A meno che tu non stia scrivendo codice C che chiama sched(7)nell'API (POSIX: vedi <sched.h>e pagine collegate da lì), in pratica non puoi avere garanzie in tempo reale di questo modulo.
Kevin,

Solo per eseguire il backup di ciò che ha detto @Kevin, l'uso di sleep () per cercare di ottenere qualsiasi tipo di temporizzazione precisa è destinato al fallimento, garantisce solo ALMENO 1 secondo di sospensione. Se hai davvero bisogno di tempi precisi devi guardare l'orologio di sistema (vedi CLOCK_MONOTONIC) e innescare le cose in base al tempo trascorso dall'ultimo evento + 1s e assicurarti di non inciampare prendendo> 1sec per correre, calcolo la prossima volta dopo qualche operazione, ecc.
John U

lascerò questo qui falsehoodsabouttime.com
Malbarez il

Proprio una volta al secondo = usa un VCXO. Una soluzione solo software ti porterà solo "abbastanza buono", ma non preciso.
Ian MacDonald,

Risposte:


65

Per stare un po 'più vicino al codice originale, quello che faccio è:

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

Questo cambia un po 'la semantica: se le tue cose impiegano meno di un secondo, aspetteranno semplicemente che passi il secondo intero. Tuttavia, se le tue cose impiegano più di un secondo per qualsiasi motivo, non continueranno a generare ancora più sottoprocessi senza mai finire.

Quindi le tue cose non funzionano mai in parallelo, e non in background, quindi anche le variabili funzionano come previsto.

Nota che se avvii anche attività in background aggiuntive, dovrai modificare le waitistruzioni per attendere solo il sleepprocesso in modo specifico.

Se ne hai bisogno per essere ancora più preciso, probabilmente dovrai solo sincronizzarlo con l'orologio di sistema e sleep ms invece di secondi interi.


Come sincronizzare l'orologio di sistema? Nessuna idea davvero, stupido tentativo:

Predefinito:

while sleep 1
do
    date +%N
done

Uscita: 003511461 010510925 016081282 021643477 028504349 03 ... (continua a crescere)

sincronizzato:

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

Uscita: 002648691 001098397 002514348 001293023 001679137 00 ... (rimane uguale)


9
Questo trucco sonno / attesa è davvero intelligente!
Philfr

Mi chiedo se tutte le implementazioni di sleepgestire secondi frazionari?
Jcaron,

1
@jcaron non tutti. ma funziona per il sonno gnu e il sonno busybox quindi non è esotico. Probabilmente potresti fare un semplice fallback come sleep 0.9 || sleep 1parametro non valido è praticamente l'unica ragione per cui il sonno non riesce mai.
frostschutz,

@frostschutz Mi aspetterei sleep 0.9di essere interpretato sleep 0da implementazioni ingenue (dato che è quello che atoifarebbe). Non sono sicuro se ciò comporterebbe effettivamente un errore.
jcaron,

1
Sono felice di vedere che questa domanda ha suscitato molto interesse. Il tuo suggerimento e la tua risposta sono molto validi. Non solo si mantiene nel secondo, ma si attacca il più vicino possibile all'intero secondo. Impressionante! (PS! In una nota a margine, è necessario installare GNU Coreutils e usarlo gdatesu macOS per far date +%Nfunzionare.)
Pubblicato il

30

Se riesci a ristrutturare il tuo loop in uno script / oneliner, il modo più semplice per farlo è con watche la sua preciseopzione.

Puoi vedere l'effetto con watch -n 1 sleep 0.5: mostrerà i secondi contando, ma occasionalmente salterà più di un secondo. Eseguendolo come watch -n 1 -p sleep 0.5verrà emesso due volte al secondo, ogni secondo e non vedrai alcun salto.


11

L'esecuzione delle operazioni in una subshell che viene eseguita come processo in background impedirebbe loro di interferire con il 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

L'unica volta "rubata" da un secondo sarebbe il tempo impiegato per lanciare la subshell, quindi alla fine salterebbe un secondo, ma si spera meno spesso del codice originale.

Se il codice nella subshell finisce per usare più di un secondo, il ciclo inizierà ad accumulare lavori in background e alla fine esaurire le risorse.


9

Un'altra alternativa (se non è possibile utilizzare, ad esempio, watch -pcome suggerisce Maelstrom) è sleepenh[ manpage ], che è progettato per questo.

Esempio:

#!/bin/sh

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

Si noti sleep 0.2che il simulatore esegue un compito dispendioso in termini di tempo, consumando circa 200 ms. Nonostante ciò, l'output dei nanosecondi rimane stabile (beh, secondo gli standard del sistema operativo non in tempo reale) - succede una volta al secondo:

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

Questo è diverso da 1 ms e nessuna tendenza. Va abbastanza bene; dovresti aspettarti un rimbalzo di almeno 10 ms se c'è un carico sul sistema, ma nel tempo non c'è alcuna deriva. Cioè, non perderai un secondo.


7

Con zsh:

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

Se il sonno non supporta floating point secondi, è possibile utilizzare zshl' zselectinvece (dopo una 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

Questi non devono andare alla deriva finché i comandi nel loop impiegano meno di un secondo per l'esecuzione.


0

Avevo esattamente lo stesso requisito per uno script di shell POSIX, in cui tutti gli helper (usleep, GNUsleep, sleepenh, ...) non erano disponibili.

vedi: 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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.