每秒精确运行一次循环


33

我正在运行此循环以每秒检查和打印一些内容。但是,由于计算可能要花费几百毫秒,因此打印时间有时会跳过一秒钟。

有什么办法可以写出这样一个循环,以确保我每秒都能得到一个打印输出?(当然,提供循环中的计算时间不到一秒钟:)

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
请注意,在大多数情况下,“ 每秒精确一次”是不可能的,因为(通常)您正在用户空间中的抢先式多任务内核上运行,该内核将按您认为合适的方式调度代码(这样一来,您可能无法在睡眠后立即重新获得控制权例如)。除非您正在编写调用该sched(7)API的C代码(POSIX:请参阅参考资料<sched.h>和从那里链接的页面),否则您基本上无法获得这种形式的实时保证。
凯文

仅仅备份@Kevin所说的话,使用sleep()尝试获得任何精确的计时注定会失败,它只能保证至少1秒钟的睡眠时间。如果您确实需要精确的计时,则需要查看系统时钟(请参阅CLOCK_MONOTONIC),并根据自上次事件发生后的时间+ 1s触发事件,并确保不要花费超过1秒的时间运行自己,计算某些操作的下一次时间,等等
约翰·U

只是要在这里离开这个falsehoodsabouttime.com
ALO Malbarez

精确地每秒一次=使用VCXO。纯软件解决方案只会使您“足够好”,但不够精确。
伊恩·麦克唐纳

Answers:


65

为了更接近原始代码,我要做的是:

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

这就稍微改变了语义:如果您的东西用了不到一秒钟,它只会等待一整秒钟。但是,如果您的东西出于任何原因花费了超过一秒钟的时间,它将永远不会产生更多的子进程,并且永远不会结束。

因此,您的工作永远不会并行运行,也不会在后台运行,因此变量也可以按预期运行。

请注意,如果您也确实启动了其他后台任务,则必须更改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 ...(保持不变)


9
这种睡眠/等待技巧真的很聪明!
philfr

我想知道是否所有sleep处理秒数的小数?
jcaron

1
@jcaron并非所有人。但它适用于gnu睡眠和busybox睡眠,因此它不是奇特的。您可能会做一个简单的回退,sleep 0.9 || sleep 1因为无效参数几乎是睡眠失败的唯一原因。
弗罗斯特斯

@frostschutz我希望sleep 0.9可以将其解释为sleep 0幼稚的实现(atoi如果这样做的话)。不知道这是否真的会导致错误。
jcaron

1
我很高兴看到这个问题引起了人们的极大兴趣。您的建议和答案都很好。它不仅保持在第二秒以内,而且还尽可能靠近整个第二秒。令人印象深刻!(!PS在一个侧面说明,一个必须安装GNU Coreutils的和使用gdate在Mac OS做出date +%N的工作。)
forthrin

30

如果您可以将循环重组为脚本/ oneliner,则最简单的方法是watch及其及其precise选项。

您可以通过看到效果watch -n 1 sleep 0.5-它会显示秒数,但偶尔会跳过一秒。以watch -n 1 -p sleep 0.5每秒的速度运行它将每秒输出两次,并且您不会看到任何跳跃。


11

在作为后台作业运行的子shell中运行这些操作将不会使它们对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

从一秒钟内“被窃”的唯一时间就是启动子Shell所花费的时间,因此它最终将跳过一秒钟,但希望比原始代码少。

如果子外壳程序中的代码使用的时间超过一秒钟,则该循环将开始积累后台作业,并最终耗尽资源。


9

另一个替代方法(例如,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。尽管如此,纳秒级输出仍然保持稳定(按非实时操作系统标准)–每秒发生一次:

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

相差不到1毫秒,而且没有趋势。那很好 如果系统上有任何负载,则应该预期至少10ms的反弹时间-但仍不会随时间漂移。即,您将不会失去任何机会。


7

zsh

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

如果您的睡眠不支持浮点秒数,则可以改用zshs zselect(在后面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

只要循环中的命令运行时间少于一秒,这些命令就不应漂移。


0

对于POSIX shell脚本,我有完全相同的要求,在该脚本中,所有助手(usleep,GNUsleep,sleepenh等)都不可用。

参见: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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.