Running a loop precisely once per second

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)


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.


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.