36

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
Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255
forthrin
  • 2,289
  • Possibly helpful: https://unix.stackexchange.com/q/60767/117549 – Jeff Schaller Aug 06 '18 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 06 '18 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 07 '18 at 12:07
  • just going to leave this here http://falsehoodsabouttime.com – alo Malbarez Aug 07 '18 at 15:36
  • Precisely once a second = use a VCXO. A software-only solution will only get you to "good enough", but not precise. – Ian MacDonald Aug 08 '18 at 14:42

6 Answers6

71

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)

frostschutz
  • 48,978
  • 11
    This sleep/wait trick is really clever ! – philfr Aug 08 '18 at 07:20
  • I'm wondering if all implementations of sleep handle fractional seconds? – jcaron Aug 09 '18 at 08: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. – frostschutz Aug 09 '18 at 08: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 09 '18 at 10:18
  • @jcaron Sure, that's possible... same goes for date +%N, it's just not available everywhere, some platforms don't measure time in micro- or even nanoseconds at all and then you just have to live with sleep 1. Personally, as long as it works on my modern-day Linux system and less-capable ebook reader, I'm happy with it. If you're targeting an embedded or archaic system you have to adapt it yourself. – frostschutz Aug 09 '18 at 11:38
  • 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 09 '18 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.

Maelstrom
  • 401
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.

Kusalananda
  • 333,661
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.

derobert
  • 109,670
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
  • 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. – Mikko Rantalainen May 11 '20 at 08: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. – Bastian Bittorf May 15 '20 at 08:32