To stay a bit closer to the original code, what I do is:
while true; do
sleep 1 &
...your stuff here...
wait # for sleep
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:
while sleep 1
Output: 003511461 010510925 016081282 021643477 028504349 03... (keeps growing)
while sleep 0.$((1999999999 - 1$(date +%N)))
Output: 002648691 001098397 002514348 001293023 001679137 00... (stays same)