Ejecutando un bucle precisamente una vez por segundo

33

Estoy ejecutando este ciclo para verificar e imprimir algunas cosas cada segundo. Sin embargo, debido a que los cálculos toman unos pocos cientos de milisegundos, el tiempo impreso a veces se salta un segundo.

¿Hay alguna forma de escribir un bucle que me garantice obtener una copia impresa cada segundo? (Siempre que, por supuesto, los cálculos en el ciclo tomen menos de un segundo :))

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
outrin
fuente
Posiblemente útil: unix.stackexchange.com/q/60767/117549
Jeff Schaller
26
Tenga en cuenta que " precisamente una vez por segundo" no es literalmente posible en la mayoría de los casos porque (generalmente) se está ejecutando en el espacio de usuario encima de un núcleo multitarea preventivo que programará su código como mejor le parezca (para que no pueda recuperar el control inmediatamente después de dormir) termina, por ejemplo). A menos que esté escribiendo un código C que llame a la sched(7)API (POSIX: ver <sched.h>y páginas enlazadas desde allí), básicamente no puede tener garantías en tiempo real de este formulario.
Kevin
Solo para respaldar lo que dijo @Kevin, usar sleep () para intentar obtener cualquier tipo de sincronización precisa está condenado al fracaso, solo garantiza AL MENOS 1 segundo de sueño. Si realmente necesita tiempos precisos, debe mirar el reloj del sistema (vea CLOCK_MONOTONIC) y activar cosas basadas en el tiempo desde el último evento + 1s, y asegúrese de no tropezarse tomando> 1 segundo para correr, calcular la próxima vez después de alguna operación, etc.
John U
solo voy a dejar esto aquí falsehoodsabouttime.com
alo Malbarez
Precisamente una vez por segundo = use un VCXO. Una solución solo de software solo lo llevará a "lo suficientemente bueno", pero no preciso.
Ian MacDonald

Respuestas:

65

Para estar un poco más cerca del código original, lo que hago es:

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

Esto cambia un poco la semántica: si sus cosas tomaron menos de un segundo, simplemente esperará a que pase el segundo completo. Sin embargo, si sus cosas tardan más de un segundo por cualquier motivo, no seguirá generando aún más subprocesos sin un final.

Por lo tanto, sus cosas nunca se ejecutan en paralelo y no en segundo plano, por lo que las variables funcionan como se esperaba

Tenga en cuenta que si también inicia tareas en segundo plano adicionales, tendrá que cambiar las waitinstrucciones para esperar solo el sleepproceso específicamente.

Si necesita que sea aún más preciso, probablemente solo tenga que sincronizarlo con el reloj del sistema y suspender ms en lugar de segundos completos.


¿Cómo sincronizar con el reloj del sistema? No tengo idea realmente, intento estúpido:

Defecto:

while sleep 1
do
    date +%N
done

Salida: 003511461 010510925 016081282 021643477 028504349 03 ... (sigue creciendo)

Sincronizado:

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

Salida: 002648691 001098397 002514348 001293023 001679137 00 ... (permanece igual)

frostschutz
fuente
99
¡Este truco de dormir / esperar es realmente inteligente!
philfr
Me pregunto si todas las implementaciones de sleepmanejar segundos fraccionarios?
jcaron
1
@jcaron no todos ellos. pero funciona para gnu sleep y busybox sleep, por lo que no es exótico. Probablemente podría hacer una reserva simple, sleep 0.9 || sleep 1como un parámetro no válido, es prácticamente la única razón para que el sueño falle.
frostschutz
@frostschutz Espero que me sleep 0.9interpreten como sleep 0implementaciones ingenuas (dado que eso es lo atoique haría). No estoy seguro si eso realmente resultaría en un error.
jcaron
1
Me alegra ver que esta pregunta despertó mucho interés. Su sugerencia y respuesta son muy buenas. No solo se mantiene dentro del segundo, sino que también se mantiene lo más cerca posible del segundo completo. ¡Impresionante! (! PS En una nota lateral, se debe instalar y el uso de GNU Coreutils gdateen MacOS para hacer date +%Nel trabajo.)
forthrin
30

Si puede reestructurar su ciclo en un script / oneliner, entonces la forma más sencilla de hacerlo es con watchy su preciseopción.

Puede ver el efecto con watch -n 1 sleep 0.5: mostrará segundos contando, pero ocasionalmente saltará más de un segundo. Ejecutarlo como se watch -n 1 -p sleep 0.5generará dos veces por segundo, cada segundo, y no verá ningún salto.

Maelstrom
fuente
11

Ejecutar las operaciones en una subshell que se ejecuta como un trabajo en segundo plano haría que no interfieran tanto con el 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

El único momento "robado" de un segundo sería el tiempo que lleva lanzar el subshell, por lo que eventualmente se saltará un segundo, pero con suerte con menos frecuencia que el código original.

Si el código en el subshell termina usando más de un segundo, el ciclo comenzará a acumular trabajos en segundo plano y, finalmente, se quedarán sin recursos.

Kusalananda
fuente
9

Otra alternativa (si no puede usar, por ejemplo, watch -pcomo sugiere Maelstrom) es sleepenh[página de manual ], que está diseñada para esto.

Ejemplo:

#!/bin/sh

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

Tenga sleep 0.2en cuenta que allí se simula hacer una tarea que consume mucho tiempo comiendo alrededor de 200 ms. A pesar de eso, la salida en nanosegundos se mantiene estable (bueno, según los estándares del sistema operativo que no es en tiempo real); ocurre una vez por segundo:

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

Eso es menos de 1 ms diferente, y no hay tendencia. Eso es bastante bueno; debe esperar rebotes de al menos 10 ms si hay alguna carga en el sistema, pero aún así no hay deriva con el tiempo. Es decir, no perderás ni un segundo.

derobert
fuente
7

Con zsh:

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

Si su sueño no admite segundos de coma flotante, puede usar zsh's zselecten su lugar (después de 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

Esos no deberían derivar mientras los comandos en el bucle tarden menos de un segundo en ejecutarse.

Stéphane Chazelas
fuente
0

Tenía exactamente el mismo requisito para un script de shell POSIX, donde todos los ayudantes (usleep, GNUsleep, sleepenh, ...) no están disponibles.

ver: 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
fuente