How To Nice A Bash Function

I was recently writing a bash script where I wanted to nice a particular function. The function in question was doing multiple expensive operations (e.g. gzip), and I didn't want to have to put a nice invocation in front of every one of these operations.

It turns out that this is kind of tricky. The nice command isn't a builtin, and therefore requires a real command to run with the new niceness value. For instance, the following bash will exit with an error due to foo not being an actual executable:

foo() {
  gzip -d bigfile.gz
  grep "hello world" /dev/urandom
}

# try to lower the priority of foo
nice foo

In this example the printed error message sill be something like nice: ‘foo’: No such file or directory.

One idea I had for overcoming this obstacle is to renice the process twice. For instance, I was initially considering the following, where I lower the nice value at function entry and then raise it again upon function exit:

foo() {
  renice 10 $$   # lower the niceness
  gzip -d bigfile.gz
  grep "hello world" /dev/urandom
  renice -10 $$  # raise the niceness again
}

Unfortunately this doesn't work because only the root user can decrease niceness. So if the script is being run as an unprivileged user the first renice command will work, but the second will fail.

The solution I came up with is to use renice in conjunction with bash subshells. The original version I had looked like this:

foo() {
  renice 10 $BASHPID  # note $BASHPID, *not* $$
  gzip -d bigfile.gz
  grep "hello world" /dev/urandom
}

# invoke foo in a subshell
(foo)

This works because the subshell will execute as a separate process, which means that there's no need to undo the renice command. Note here that $BASHPID must be used here, instead of $$ which is much more commonly used to locate the current process id. The reason is that bash subshells actually return the original shell's PID when using $$, whereas $BASHPID always returns the actual PID for the process.

The downside to this approach---which I think is significant---is that if you forget to invoke the function in a subshell then the entire script will end up niced. For instance, given the previous definition for foo we'll have the following correct and incorrect invocations:

(foo)  # CORRECT: uses a subshell
foo    # INCORRECT: now the entire script is niced

This is pretty subtle. To understand what's happening here you need to know what a subshell is, what the syntax is for subshell invocation, and you need to understand why a subshell is needed in the first place.

My friend James pointed out that this can be fixed by invoking the subshell within the function. That looks like this:

foo() {
  (renice 10 $BASHPID
   gzip -d bigfile.gz
   grep "hello world" /dev/urandom)
}

Now foo can be invoked as normal. Nifty!