Running Work in Parallel: &, wait and xargs -P

Commands that each take a while can run side by side: & starts each one in the background and wait holds the script until all of them have finished, while xargs -P runs a set number at a time, so 12 one-second photo resizes take 3 seconds instead of 12.

Freelancer · Unix · Linux · Updated

After this lesson you can

  • Run commands at the same time with & and wait until they've all finished with wait
  • Run a command on many files a few at a time with xargs -n 1 -P, and measure the time saved with time

Key takeaways

  • A for loop runs its commands one after another. time in front of a command shows how long it took: real is the time on the clock.
  • An & after a command in a loop starts every one at once. wait then holds the script until all of them have finished.
  • Hundreds of jobs at once can swamp a computer. xargs -n 1 -P 4 runs four at a time, one name each, starting the next as soon as one ends.
  • find ... -print0 | xargs -0 keeps names with spaces whole. On Linux, xargs exits with 123 when any of its commands failed.
  • Parallel jobs finish in any order, so their lines arrive mixed. Give each job its own output file.

You're home from a trip with 500 photos straight off the camera, and you want small copies to share. Resizing one takes about a second, so a loop that does them one by one (For Every File: for Loops) takes over eight minutes. Meanwhile the computer is mostly idle. Its processor has several cores, each able to work on a job of its own, and a loop keeps only one of them busy, like a bakery with four ovens that bakes one tray at a time. Doing several jobs side by side is called parallel processing. A web developer shrinking a site's images uses it, so does a data analyst converting a thousand files, and so does an administrator compressing a month of logs.

One at a time

Make the practice folder: twelve pretend photos (one with a space in its name), a packing list that ended up among them, and resize.sh. It stands in for a real resizer such as ImageMagick, so you don't need to install anything: like resizing one big photo, it takes about a second, and it puts a copy in small/. (With ImageMagick, the real work is a line such as magick photo.jpg -resize 1200x small/photo.jpg; it isn't part of this lesson.)

mkdir -p ~/terminal-practice/resize/photos
cd ~/terminal-practice/resize
touch photos/photo-{01..11}.jpg "photos/beach day.jpg"
printf '%s\n' 'sunscreen' 'hat' > photos/packing-list.txt
printf '%s\n' '#!/usr/bin/env bash' '# resize.sh PHOTO: a stand-in for a real photo resizer, such as ImageMagick.' '# Like resizing one big photo, it takes about a second. The small copy goes into small/.' '[[ "$1" == *.jpg ]] || { echo "resize.sh: $1 is not a photo" >&2; exit 1; }' 'sleep 1' 'mkdir -p small' 'cp "$1" "small/$(basename "$1")"' 'echo "Resized $(basename "$1")"' > resize.sh
chmod +x resize.sh

time in front of a command runs it, then says how long it took:

time for p in photos/*.jpg; do ./resize.sh "$p"; done
Resized beach day.jpg
Resized photo-01.jpg
Resized photo-02.jpg
Resized photo-03.jpg
Resized photo-04.jpg
Resized photo-05.jpg
Resized photo-06.jpg
Resized photo-07.jpg
Resized photo-08.jpg
Resized photo-09.jpg
Resized photo-10.jpg
Resized photo-11.jpg

real	0m12.149s
user	0m0.050s
sys	0m0.052s

real is the time on the clock: twelve photos, twelve seconds. user and sys count the processor's own work, which is tiny here because the stand-in spends its second waiting.

All at once: & and wait

An & after a command starts it in the background (Background Jobs: &, jobs, fg and bg) and moves straight on. Inside a loop, that starts every resize at once. wait then holds the script until every background job it started has finished. Save this as all-at-once.sh:

#!/usr/bin/env bash
# all-at-once.sh: resizes every photo in photos/ at the same time.
for p in photos/*.jpg; do
  ./resize.sh "$p" &
done
wait
echo "All done."
chmod +x all-at-once.sh
time ./all-at-once.sh
Resized photo-02.jpg
Resized photo-09.jpg
Resized photo-05.jpg
Resized photo-01.jpg
Resized photo-11.jpg
Resized photo-10.jpg
Resized photo-04.jpg
Resized photo-07.jpg
Resized photo-03.jpg
Resized photo-08.jpg
Resized photo-06.jpg
Resized beach day.jpg
All done.

real	0m1.018s
user	0m0.082s
sys	0m0.051s

One second for all twelve. They finished in a jumbled order, because twelve jobs racing side by side end whenever each is done. Leave out wait, and All done. is printed first, before a single photo is resized, and the Resized lines turn up after your prompt has come back. A script that zipped up small/ next would zip an empty folder. (In a script, bash doesn't print the [1] 3124 job lines it shows at the prompt.)

A few at a time: xargs -P

Twelve at once is fine. Five hundred at once is not: each real resize holds a whole photo in memory, and hundreds of them fight over a few cores until everything slows down. A bakery with four ovens bakes four trays at a time, and puts the next tray in as soon as one comes out. xargs from Tee and Xargs: Splitting and Feeding does exactly that with -P (processes at once). First, see how many cores you have:

nproc
24

This computer has 24; many laptops have 4 to 16. (On a Mac, sysctl -n hw.ncpu tells you.) Now four at a time:

time find photos -name '*.jpg' -print0 | xargs -0 -n 1 -P 4 ./resize.sh
Resized photo-01.jpg
Resized photo-04.jpg
Resized photo-08.jpg
Resized photo-03.jpg
Resized photo-07.jpg
Resized photo-06.jpg
Resized photo-05.jpg
Resized photo-11.jpg
Resized beach day.jpg
Resized photo-09.jpg
Resized photo-02.jpg
Resized photo-10.jpg

real	0m3.038s
user	0m0.072s
sys	0m0.048s

Three rounds of four: three seconds.

Three ways to resize twelve one-second photos, drawn as timelines. One at a time: one row of twelve boxes, 12 seconds. All at once with & and wait: twelve rows of one box each, 1 second. Four at a time with xargs -P 4: four rows of three boxes each, 3 seconds

Read it from the left: find lists the photos, ending each name with an invisible marker (-print0), and xargs -0 splits only at that marker, so beach day.jpg stays one name. -n 1 hands each resize.sh one name, and -P 4 keeps four running. A good number to start with is your number of cores, or fewer. xargs finishes only when the last job does, so it needs no wait.

When one fails

Leave out -name '*.jpg', and the packing list goes to resize.sh too:

find photos -type f -print0 | xargs -0 -n 1 -P 4 ./resize.sh
echo $?
Resized photo-04.jpg
Resized photo-01.jpg
Resized photo-03.jpg
Resized photo-08.jpg
Resized photo-07.jpg
Resized photo-06.jpg
Resized photo-11.jpg
Resized photo-05.jpg
Resized beach day.jpg
Resized photo-09.jpg
Resized photo-02.jpg
Resized photo-10.jpg
resize.sh: photos/packing-list.txt is not a photo
123

The photos were all done, the one failure was reported, and xargs carried on. Its exit code, 123 on Linux, means "at least one command failed", so &&, || and if notice (Did It Work? Exit Codes, && and ||). In a long run the error line can hide among hundreds of others, so check the exit code, not the screen. And because the jobs run side by side, give each one its own output file, as resize.sh does: two jobs writing into the same file can mix their lines together.

Check yourself

2 questions to try on the lesson page.

So what

When a job repeats on many files and each one takes a while, running a few at a time with xargs -P, or all together with & and wait, turns minutes into seconds.

Key terms

Cheat sheets

Related