Executando um loop precisamente uma vez por segundo

32

Estou executando esse loop para verificar e imprimir algumas coisas a cada segundo. No entanto, como os cálculos demoram talvez algumas centenas de milissegundos, o tempo impresso às vezes pula um segundo.

Existe alguma maneira de escrever um loop que eu garanta uma impressão a cada segundo? (Desde que, claro, os cálculos no loop demorem menos de um 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
    
por forthrin 06.08.2018 / 16:57

5 respostas

62

Para ficar um pouco mais perto do código original, o que eu faço é:

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

Isso muda um pouco a semântica: se o seu material levar menos de um segundo, ele simplesmente esperará passar o segundo inteiro. No entanto, se o seu material demorar mais de um segundo por qualquer motivo, ele não continuará gerando ainda mais subprocessos sem nunca ter fim.

Assim, suas coisas nunca são executadas em paralelo, e não em segundo plano, então as variáveis funcionam como o esperado.

Observe que, se você iniciar tarefas adicionais em segundo plano, terá que alterar a instrução wait para aguardar apenas o processo sleep especificamente.

Se você precisar que ele seja ainda mais preciso, você provavelmente só precisará sincronizá-lo com o clock do sistema e dormir ms em vez de segundos completos.

Como sincronizar com o relógio do sistema? Nenhuma ideia, tentativa estúpida:

Padrão:

while sleep 1
do
    date +%N
done

Saída: 003511461 010510925 016081282 021643477 028504349 03 ... (continua crescendo)

sincronizado:

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

Saída: 002648691 001098397 002514348 001293023 001679137 00 ... (permanece mesmo)

    
por 06.08.2018 / 17:20
30

Se você puder reestruturar seu loop em um script / oneliner, a maneira mais simples de fazer isso é com watch e sua opção precise .

Você pode ver o efeito com watch -n 1 sleep 0.5 - ele mostrará a contagem de segundos, mas ocasionalmente passará um segundo. Executá-lo como watch -n 1 -p sleep 0.5 produzirá duas vezes por segundo, a cada segundo, e você não verá nenhum salto.

    
por 07.08.2018 / 02:10
10

A execução das operações em um subshell executado como um job em background faria com que elas não interferissem tanto com o 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

O único momento "roubado" do primeiro segundo seria o tempo necessário para lançar o subshell, de modo que acabaria pulando um segundo, mas esperançosamente com menos frequência do que o código original.

Se o código na subshell acabar usando mais do que um segundo, o loop começaria a acumular tarefas em segundo plano e, eventualmente, ficaria sem recursos.

    
por 06.08.2018 / 17:02
8

Outra alternativa (se você não pode usar, por exemplo, watch -p como Maelstrom sugere) é sleepenh [ manpage ], projetado para isso.

Exemplo:

#!/bin/sh

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

Observe que o sleep 0.2 no simulate faz uma tarefa demorada comendo cerca de 200ms. Apesar disso, a saída de nanossegundos permanece estável (bem, pelos padrões de sistemas operacionais não reais) - acontece uma 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

Isso é menos de 1 ms e não há tendência. Isso é muito bom; Você deve esperar pelo menos 10ms de rejeição se houver alguma carga no sistema - mas ainda assim não haverá desvios com o passar do tempo. Ou seja, você não perderá um segundo.

    
por 07.08.2018 / 19:41
7

com zsh :

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

Se o seu sono não suportar segundos de ponto flutuante, você pode usar zsh zselect (depois de 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

Esses não devem ser alterados enquanto os comandos no loop levarem menos de um segundo para serem executados.

    
por 07.08.2018 / 08:42