web-dev-qa-db-ja.com

1秒に1回正確にループを実行する

このループを実行して、毎秒いくつかのことをチェックして出力します。ただし、計算には数百ミリ秒かかる場合があるため、出力された時間は1秒をスキップすることがあります。

毎秒プリントアウトすることが保証されているようなループを書く方法はありますか? (もちろん、ループ内の計算にかかる時間は1秒未満です:))

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

元のコードに少し近づけるために、私は次のことを行います。

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

これはセマンティクスを少し変更します。もしも1秒もかからない場合は、1秒が経過するのを待つだけです。ただし、何かが何らかの理由で1秒以上かかる場合は、終了することなく、さらに多くのサブプロセスを生成し続けることはありません。

そのため、バックグラウンドではなく並列に実行されることはないため、変数も期待どおりに機能します。

追加のバックグラウンドタスクも開始する場合は、wait命令を変更して、具体的にはsleepプロセスのみを待機する必要があることに注意してください。

さらに正確にする必要がある場合は、システムクロックに同期して、秒単位ではなくmsをスリープさせるだけで済みます。


システムクロックに同期する方法まったくわからない、愚かな試み:

デフォルト:

while sleep 1
do
    date +%N
done

出力:003511461 010510925 016081282 021643477 028504349 03 ...(増加し続ける)

同期済み:

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

出力:002648691 001098397 002514348 001293023 001679137 00 ...(同じまま)

65
frostschutz

ループをスクリプト/ワンライナーに再構築できる場合、これを行う最も簡単な方法は watch とそのpreciseオプションを使用することです。

watch -n 1 sleep 0.5で効果を確認できます-秒のカウントアップが表示されますが、1秒をスキップする場合があります。 watch -n 1 -p sleep 0.5として実行すると、毎秒2回、毎秒出力され、スキップは表示されません。

30
Maelstrom

バックグラウンドジョブとして実行されるサブシェルで操作を実行すると、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

1秒から「盗まれた」唯一の時間は、サブシェルを起動するのにかかった時間になるため、最終的には1秒をスキップしますが、元のコードよりも頻度が低いことが望まれます。

サブシェルのコードが1秒以上moreを使用して終了すると、ループはバックグラウンドジョブを蓄積し始め、最終的にリソースが不足します。

11
Kusalananda

別の代替方法(使用できない場合、たとえば、watch -p Maelstromが示唆するように) sleepenh [ manpage ]は、このために設計されています。

例:

#!/bin/sh

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

sleep 0.2シミュレーションでは、200ms程度の時間を消費するタスクを実行します。それにもかかわらず、ナノ秒出力は安定しています(非リアルタイムOS標準によります)。毎秒1回発生します。

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

それは1ms未満であり、傾向はありません。それはとても良いことです。システムに負荷がかかっている場合、少なくとも10ミリ秒のバウンスが予想されますが、時間の経過によるドリフトはありません。つまり、あなたは秒を失うことはありません。

9
derobert

zshの場合:

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

睡眠が浮動小数点秒をサポートしていない場合は、代わりにzshzselectを使用できます(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

ループ内のコマンドの実行に1秒未満かかる限り、これらはドリフトしないはずです。

7

すべてのヘルパー(usleep、GNUsleep、sleepenhなど)が使用できないPOSIXシェルスクリプトとまったく同じ要件がありました。

参照してください: 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