30

I know I can wait on a condition to become true in bash by doing:

while true; do
  test_condition && break
  sleep 1
done

But it creates 1 sub-process at each iteration (sleep). I could avoid them by doing:

while true; do
  test_condition && break
done

But it uses lot of CPU (busy waiting). To avoid sub-processes and busy waiting, I came up with the solution bellow, but I find it ugly:

my_tmp_dir=$(mktemp -d --tmpdir=/tmp)    # Create a unique tmp dir for the fifo.
mkfifo $my_tmp_dir/fifo                  # Create an empty fifo for sleep by read.
exec 3<> $my_tmp_dir/fifo                # Open the fifo for reading and writing.

while true; do
  test_condition && break
  read -t 1 -u 3 var                     # Same as sleep 1, but without sub-process.
done

exec 3<&-                                # Closing the fifo.
rm $my_tmp_dir/fifo; rmdir $my_tmp_dir   # Cleanup, could be done in a trap.

Note: in the general case, I cannot simply use read -t 1 var without the fifo, because it will consume stdin, and will not work if stdin is not a terminal or a pipe.

Can I avoid sub-processes and busy waiting in a more elegant way ?

jfg956
  • 6,336
  • 1
    true is a builtin and does not create a sub process in bash. busy waiting will always be bad. – jordanm Mar 17 '13 at 17:47
  • @joranm: you are right about true, question updated. – jfg956 Mar 17 '13 at 17:59
  • Why not without fifo? Simply read -t 1 var. – ott-- Mar 17 '13 at 18:21
  • @ott: you are right, but this will consume stdin. Also, it will not work if stdin is not a terminal or a pipe. – jfg956 Mar 17 '13 at 18:23
  • If maintainability is an issue, I'd would strongly suggest going with the sleep as in the first example. The second one, while it may work, is not going to be easy for anyone to adjust in the future. Simple code also have bigger potential for being safe. – Kusalananda Jan 31 '18 at 09:33
  • If stdin is closed but stdout or stderr is still open it is possible to do "exec <&1/2", although I don't know how portable that is. – thabubble Apr 01 '18 at 08:44

8 Answers8

23

In newer versions of bash (at least v2), builtins may be loaded (via enable -f filename commandname) at runtime. A number of such loadable builtins is also distributed with the bash sources, and sleep is among them. Availability may differ from OS to OS (and even machine to machine), of course. For example, on openSUSE, these builtins are distributed via the package bash-loadables.

PhilR
  • 149
  • Wow, this is what I am looking for, and I definitely learn something about loadable builtin: +1. I will try this, and yet it is the best answer. – jfg956 Mar 18 '13 at 09:28
  • 1
    It works ! On debian, the package is bash-builtins. It only includes sources and the Makefile must be edited, but I was able to install sleep as a builtin. Thanks. – jfg956 Mar 18 '13 at 12:46
12

Creating a lot of subprocesses is a bad thing in an inner loop. Creating one sleep process per second is OK. There's nothing wrong with

while ! test_condition; do
  sleep 1
done

If you really want to avoid the external process, you don't need to keep the fifo open.

my_tmpdir=$(mktemp -d)
trap 'rm -rf "$my_tmpdir"' 0
mkfifo "$my_tmpdir/f"

while ! test_condition; do
  read -t 1 <>"$my_tmpdir/f"
done
tdaitx
  • 165
  • You are right about a process per second being peanuts (but my question was about finding a way to remove it). About the shorter version, It is nicer than mine, so +1 (but I removed the mkdir as it is done by mktemp (if not, it is a race condition)). Also true about the while ! test_condition; which is nicer than my initial solution. – jfg956 Mar 18 '13 at 09:25
10

I recently had a need to do this. I came up with the following function that will allow bash to sleep forever without calling any external program:

snore()
{
    local IFS
    [[ -n "${_snore_fd:-}" ]] || { exec {_snore_fd}<> <(:); } 2>/dev/null ||
    {
        # workaround for MacOS and similar systems
        local fifo
        fifo=$(mktemp -u)
        mkfifo -m 700 "$fifo"
        exec {_snore_fd}<>"$fifo"
        rm "$fifo"
    }
    read ${1:+-t "$1"} -u $_snore_fd || :
}

NOTE: I previously posted a version of this that would open and close the file descriptor each time, but I found that on some systems doing this hundreds of times a second would eventually lock up. Thus the new solution keeps the file descriptor between calls to the function. Bash will clean it up on exit anyway.

This can be called just like /bin/sleep, and it will sleep for the requested time. Called without parameters, it will hang forever.

snore 0.1  # sleeps for 0.1 seconds
snore 10   # sleeps for 10 seconds
snore      # sleeps forever

There's a writeup with excessive details on my blog here

bolt
  • 141
  • 1
  • 3
  • 1
    Excellent blog entry. However, I went there looking for an explanation why read -t 10 < <(:) returns immediately while read -t 10 <> <(:) waits the full 10 seconds, but I still don’t get it. – Amir Jul 02 '19 at 10:14
  • In read -t 10 <> <(:) what does <> stand for? – CodeMedic Jul 02 '19 at 12:37
  • <> opens the file descriptor for reading and writing, even though the underlying process substitution <(:) only allows reading. This is a hack that causes Linux, and Linux specifically, to assume someone might write to it, so read will hang waiting for input that will never arrive. It will not do this on BSD systems, in which case the workaround will kick in. – bolt Jul 15 '19 at 08:04
  • Just read -t 10 <> <(:) without creating unique file descriptors beforehand seems to be working fine with multiple concurrent calls (linux, bash 4.1) – Normadize Dec 28 '19 at 23:48
  • See the "NOTE" I posted for why I'm not doing that. On some systems, mainly the Raspberry Pi's I was testing it on, they locked up after a few thousand calls. Some bug I didn't want to track down. Keeping the descriptor open also saves on processing power, as your call to (:) will spawn a subshell each time. It exits immediately, sure, but it's still spawned. – bolt Dec 30 '19 at 00:21
  • @bolt Is there any way to cancel the infinite snore? I trap the SIGTERM signal, and want to cancel the snore from there, else the script will not exit by itself. So is there some command I can use that will end the snore? – Maestro Apr 16 '23 at 23:52
  • @Maestro it is blocked on IO, so not easy. Perhaps you can adapt the formula using a fifo, as it is already for macOS, but without "forgetting" and deleting fifo. Then you can echo a blank line to wake all those sleeping. – CodeMedic Oct 14 '23 at 09:40
  • @Maestro try this. wakey_wakey() { [[ -v _snore_fd ]] && echo >&$_snore_fd || :; } – CodeMedic Oct 15 '23 at 01:53
  • Also, check the blog post for an updated function, as the latest versions of bash cause the subshell to hang around until the first time you finish reading from it. ...in case you care about that subshell. – bolt Oct 15 '23 at 11:44
4

As user yoi said, if in your script is stdin opened, then instead of sleep 1 you can simply use:

read -t 1 3<&- 3<&0 <&3

In Bash version 4.1 and newer you can use float number, e.g. read -t 0.3 ...

If in a script stdin is closed (script is called my_script.sh < /dev/null &), then you need use another opened descriptor, which not produces output when read is executed, eg. stdout:

read -t 1 <&1 3<&- 3<&0 <&3

If in a script all descriptor is closed (stdin, stdout, stderr) (e.g. because is called as daemon), then you need find any exists file which not produces output:

read -t 1 </dev/tty10 3<&- 3<&0 <&3
Mysak
  • 49
3

In ksh93 or mksh, sleep is a shell builtin, so an alternative might be to use those shells instead of bash.

zsh also has a zselect builtin (loaded with zmodload zsh/zselect) that can sleep for a given number of hundredths of seconds with zselect -t <n>.

Scrutinizer
  • 1,142
  • 5
  • 7
2

This works from a login shell as well as a non-interactive shell.

#!/bin/sh

# to avoid starting /bin/sleep each time we call sleep, 
# make our own using the read built in function
xsleep()
{
  read -t $1 -u 1
}

# usage
xsleep 3
Omar
  • 41
  • This also worked on Mac OS X v10.12.6 – b01 May 31 '18 at 14:15
  • 1
    This is not recommended. If multiple scripts use this at the same time then they all get SIGSTOP'ed as they all try to read stdin. Your stdin gets blocked while this waits. Don't use stdin for this. You want new different file descriptors. – Normadize Sep 22 '18 at 12:28
  • 1
    @Normadize There is another answer here (https://unix.stackexchange.com/a/407383/147685) that deals with the concern of using free file descriptors. Its bare minimum version is read -t 10 <> <(:). – Amir Jul 02 '19 at 11:57
  • @Amir consider that the <(:) will create a subshell , so its not free ... – humanityANDpeace Sep 26 '23 at 08:05
  • read always returns "bad exitcode" though, and using a read -t 1 && true beats the purpose because now you're calling the true binary. So no, I don't think it works. – Hi-Angel Oct 20 '23 at 10:18
1

A slight improvement on the above mentioned solutions (which I have based this on).

bash_sleep() {
    read -rt "${1?Specify sleep interval in seconds}" -u 1 <<<"" || :;
}

# sleep for 10 seconds
bash_sleep 10

Reduced the need for a fifo and hence no cleaning up to do.

CodeMedic
  • 111
  • 1
    This is not recommended. If multiple scripts use this at the same time then they all get SIGSTOP'ed as they all try to read stdin. Your stdin gets blocked while this waits. Don't use stdin for this. You want new different file descriptors. – Normadize Sep 22 '18 at 12:26
  • @Normadize Never thought of that; please can you elaborate or point me to a resource where I can read more about it. – CodeMedic Sep 22 '18 at 13:50
  • 1
    @CodeMedic There is another answer here (https://unix.stackexchange.com/a/407383/147685) that deals with the concern of using free file descriptors. Its bare minimum version is read -t 10 <> <(:). – Amir Jul 02 '19 at 11:59
0

Do you really need a fifo? Redirecting stdin to another file descriptor should work as well.

{
echo line | while read line; do
   read -t 1 <&3
   echo "$line"
done
} 3<&- 3<&0

Inspired by: Read input in bash inside a while loop

yoi
  • 1