35

I'm running this loop to check and print some things every second. However, because the calculations take maybe a few hundred milliseconds, the printed time sometimes skip a second.

Is there any way to write such a loop that I am guaranteed to get a printout every second? (Provided, of course, that the calculations in the loop take less than a second :))

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
5
  • Possibly helpful: unix.stackexchange.com/q/60767/117549
    – Jeff Schaller
    Aug 6, 2018 at 15:18
  • 27
    Note that "precisely once per second" is not literally possible in most cases because you are (usually) running in userspace atop a preemptively multitasking kernel which will schedule your code as it sees fit (so that you might not regain control immediately after a sleep ends, for example). Unless you are writing C code which calls into the sched(7) API (POSIX: see <sched.h> and pages linked from there), you basically cannot have real-time guarantees of this form.
    – Kevin
    Aug 6, 2018 at 23:57
  • Just to back up what @Kevin said, using sleep() to try and get any sort of precise timing is doomed to failure, it only guarantees AT LEAST 1sec of sleep. If you really need precise timings you need to look at the system clock (see CLOCK_MONOTONIC) and trigger things based on time-since-last-event + 1s, and make sure you don't trip yourself up by taking >1sec to run, calculating the next time after some operation, etc.
    – John U
    Aug 7, 2018 at 12:07
  • just going to leave this here falsehoodsabouttime.com Aug 7, 2018 at 15:36
  • Precisely once a second = use a VCXO. A software-only solution will only get you to "good enough", but not precise. Aug 8, 2018 at 14:42

6 Answers 6

70

To stay a bit closer to the original code, what I do is:

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

This changes the semantics a little: if your stuff took less than a second, it will simply wait for the full second to pass. However, if your stuff takes longer than a second for any reason, it won't keep spawning even more subprocesses with never any end to it.

So your stuff never runs in parallel, and not in the background, so variables work as expected too.

Note that if you do start additional background tasks as well, you'll have to change the wait instruction to only wait for the sleep process specifically.

If you need it to be even more accurate, you'll probably just have to sync it to the system clock and sleep ms instead of full seconds.


How to sync to system clock? No idea really, stupid attempt:

Default:

while sleep 1
do
    date +%N
done

Output: 003511461 010510925 016081282 021643477 028504349 03... (keeps growing)

Synced:

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

Output: 002648691 001098397 002514348 001293023 001679137 00... (stays same)

6
  • 11
    This sleep/wait trick is really clever !
    – philfr
    Aug 8, 2018 at 7:20
  • I'm wondering if all implementations of sleep handle fractional seconds?
    – jcaron
    Aug 9, 2018 at 8:15
  • 1
    @jcaron not all of them. but it works for gnu sleep and busybox sleep so it's not exotic. You could probably do a simple fallback like sleep 0.9 || sleep 1 as invalid parameter is pretty much the only reason for sleep to ever fail. Aug 9, 2018 at 8:34
  • @frostschutz I'd expect sleep 0.9 to be interpreted as sleep 0 by naïve implementations (given that's what atoi would do). Not sure if that would actually result in an error.
    – jcaron
    Aug 9, 2018 at 10:18
  • 1
    I'm happy to see this question sparked a lot of interest. Your suggestion and answer are very good. Not only does it keep within the second, it also sticks as close to the whole second as possible. Impressive! (PS! On a side note, one must install GNU Coreutils and use gdate on macOS to make date +%N work.)
    – forthrin
    Aug 9, 2018 at 12:16
30

If you can restructure your loop into a script / oneliner then the simplest way to do this is with watch and its precise option.

You can see the effect with watch -n 1 sleep 0.5 - it will show seconds counting up, but will occasionally skip over a second. Running it as watch -n 1 -p sleep 0.5 will output twice per second, every second, and you won't see any skips.

12

Running the operations in a subshell that runs as a background job would make them not interfere so much with the 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

The only time "stolen" from the one second would be the time taken to launch the subshell, so it would eventually skip a second, but hopefully less often than the original code.

If the code in the subshell ends up using more than a second, the loop would start to accumulate background jobs and eventually run out of resources.

9

Another alternative (if you can't use, e.g., watch -p as Maelstrom suggests) is sleepenh [manpage], which is designed for this.

Example:

#!/bin/sh

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

Note the sleep 0.2 in there the simulate doing some time-consuming task eating around 200ms. Despite that, the nanoseconds output remain stable (well, by non-realtime OS standards) — it happens once per second:

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

That's under 1ms different, and no trend. That's quite good; you should expect bounces of at least 10ms if there is any load on the system — but still no drift over time. I.e., you won't lose a second.

7

With zsh:

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

If your sleep doesn't support floating point seconds, you can use zsh's zselect instead (after 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

Those should not drift as long as the commands in the loop take less than one second to run.

1

I had the exact same requirement for a POSIX shell script, where all the helpers (usleep, GNUsleep, sleepenh, ...) are not available.

see: 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
2
  • Warning: this implements busy wait always eating one full CPU core for the loop. One could try to test if perl is available with Time::HiRes and run usleep() via perl, instead. Another option would be python and time.usleep. May 11, 2020 at 8:09
  • 1
    You are right. Using e.g. perl or python, which themselves use time for startup is another problem. The above is scripted with so called shell-builtins. These are native commands, which have nearly 0 execution/run/startup time. But yes, this approach eats up 1 cpu. May 15, 2020 at 8:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.