Executando um loop precisamente uma vez por segundo

33

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, que 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
fortrina
fonte
Possivelmente útil: unix.stackexchange.com/q/60767/117549
Jeff Schaller
26
Observe que " precisamente uma vez por segundo" não é literalmente possível na maioria dos casos, porque você (normalmente) está executando no espaço do usuário no topo de um kernel preventivamente multitarefa que agendará seu código como achar melhor (para que você não possa recuperar o controle imediatamente após um sono) termina, por exemplo). A menos que você esteja escrevendo o código C que chama a sched(7)API (POSIX: consulte <sched.h>e páginas vinculadas a partir daí), você basicamente não pode ter garantias em tempo real deste formulário.
Kevin
Apenas para fazer backup do que @Kevin disse, usar sleep () para tentar obter qualquer tipo de tempo preciso está fadado ao fracasso, isso garante apenas PELO MENOS 1 segundo de sono. Se você realmente precisa de horários precisos, precisa observar o relógio do sistema (consulte CLOCK_MONOTONIC) e acionar as coisas com base nos + 1s de tempo desde o último evento e certifique-se de não se excitar tomando 1 segundo para executar, o cálculo da próxima vez após alguma operação, etc.
John L
apenas indo para deixar isso aqui falsehoodsabouttime.com
alo Malbarez
Precisamente uma vez por segundo = use um VCXO. Uma solução apenas de software fará com que você seja "bom o suficiente", mas não preciso.
11138 Ian MacDonald

Respostas:

65

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

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

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

Portanto, suas coisas nunca são executadas em paralelo, e não em segundo plano; portanto, as variáveis ​​também funcionam como esperado.

Observe que, se você iniciar tarefas adicionais em segundo plano, precisará alterar as waitinstruções para aguardar apenas o sleepprocesso especificamente.

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


Como sincronizar com o relógio do sistema? Nenhuma ideia realmente, 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 o mesmo)

frostschutz
fonte
9
Esse truque de sono / espera é realmente inteligente!
philfr
Gostaria de saber se todas as implementações de sleeplidar com segundos fracionários?
jcaron
1
@ jcaron nem todos eles. mas funciona para o sono gnu e o busybox, por isso não é exótico. Você provavelmente poderia fazer um fallback simples como sleep 0.9 || sleep 1parâmetro inválido é praticamente o único motivo para o sono falhar.
Frostschutz 9/0818
@frostschutz Eu espero sleep 0.9ser interpretado como sleep 0por implementações ingênuas (dado que é isso atoique faria). Não tenho certeza se isso realmente resultaria em um erro.
jcaron
1
Fico feliz em ver que essa pergunta despertou muito interesse. Sua sugestão e resposta são muito boas. Ele não apenas fica dentro do segundo, mas também fica o mais próximo possível do segundo. Impressionante! (! PS Em uma nota lateral, é preciso instalar GNU Coreutils e uso gdateno MacOS para fazer date +%No trabalho.)
forthrin
30

Se você pode reestruturar seu loop em um script / oneliner, a maneira mais simples de fazer isso é com watche sua preciseopção.

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

Redemoinho
fonte
11

A execução das operações em um subshell que é executado como um trabalho em segundo plano os faria não interferir 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 tempo "roubado" a partir de um segundo seria o tempo necessário para iniciar o subshell, de modo que ele eventualmente pularia um segundo, mas esperançosamente com menos frequência do que o código original.

Se o código no subshell acabar usando mais de um segundo, o loop começará a acumular trabalhos em segundo plano e, eventualmente, ficar sem recursos.

Kusalananda
fonte
9

Outra alternativa (se você não pode usar, por exemplo, watch -pcomo Maelstrom sugere) é sleepenh[página de manual ], projetada 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 o sleep 0.2simulador fazendo alguma tarefa demorada, comendo cerca de 200ms. Apesar disso, a saída em nanossegundos permanece estável (bem, pelos padrões do SO não em tempo real) - isso 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 está abaixo de 1ms diferente, e nenhuma tendência. Isso é muito bom; você deve esperar pulos de pelo menos 10ms se houver alguma carga no sistema - mas ainda assim não houver desvios ao longo do tempo. Ou seja, você não vai perder um segundo.

derobert
fonte
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's zselect(depois 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

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

Stéphane Chazelas
fonte
0

Eu tinha exatamente o mesmo requisito para um script de shell POSIX, onde todos os ajudantes (usleep, GNUsleep, sleepenh, ...) não estão disponíveis.

consulte: 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
fonte