Logo

dev-resources.site

for different kinds of informations.

Poor man's parallel in Bash

Published at
1/10/2025
Categories
shell
automation
concurrency
performance
Author
stolzenhain
Author
11 person written this
stolzenhain
open
Poor man's parallel in Bash

Original post on my blog, happy to include feedback!
Cover: Paris bibliothéques, via Clawmarks

Topics:

  1. Running scripts in parallel
  2. Tools to limit concurrent jobs
  3. Shell process handling cheatsheet
  4. Limit concurrent jobs with Bash
  5. Bonus: One-liners

1) Running scripts in parallel

does not take much effort: I've been speeding up builds by running commands simultaneously with an added & ampersand:

# stuff can happen concurrently
# use `&` to run in a sub-shell
cmd1 &
cmd2 &
cmd3 &
# wait on sub-processes
wait
# these need to happen sequentially
cmd3
cmd4

echo Done!
Enter fullscreen mode Exit fullscreen mode

Job control is a shell feature: commands are put into a background process and run at the same time.

Now assuming you want to loop over more than a few commands, e.g. converting files:

for file in *.jpg; do
    # start optimizing every file at once
    jpegoptim -m 45 "${file}" &
done
# finish queue
wait
Enter fullscreen mode Exit fullscreen mode

Running a lot of processes this way is still faster than a regular loop. But compared to just a few concurrent jobs there are no speed gains – even possible slowdowns on async disk I/O [Quotation needed].

So you'll want to use

2) Tools to limit concurrent jobs

by either 1) installing custom tools like parallel or xjobs or 2) relying on xargs, which is a feature-rich tool but more complicated.

Transforming wait to xargs code is described here: an example for parallel batch jobs. The article notes small differences between POSIX flavours – e.g. different handling of separators on BSD/MacOS.

We'll be choosing option 3) – digging into features of wait and jobs to manage processes.

Quoting this great summary, here are some example commands for

3) Shell process handling

# run child process, save process id via `$!`
cmd3 & pid=$!
# get job list
jobs
# get job ids only
# note: not available on zsh
jobs -p
# only wait on job at position `n`
# note: slots may turn up empty while
#       newer jobs rest in the queue's tail
wait %n
# wait on last job in list
wait %%
# wait on next finishing process
# note: needs Bash 4.3
wait -n
Enter fullscreen mode Exit fullscreen mode

Taking our example from before, we make sure to

4) Limit concurrent jobs with Bash

each time a process is finished using wait -n:

for file in *.jpg; do

    jpegoptim -m 45 "${file}" &

    # still < 3 max job -l ines? continue loop
    if [[ $(jobs|wc -l) -lt 3 ]]; then continue; fi

    # with 3 jobs, wait for -n ext, then loop
    wait -n

done
# finish queue
wait
Enter fullscreen mode Exit fullscreen mode

Sadly, this won't work in MacOS, because Bash environments are frozen on old versions. We replace the wait -n command with wait %% to loop on the 3rd/last job in queue – an ok compromise on small groups (1/3 chance of fastest/slowest/medium job):

for file in *.jpg; do

    jpegoptim -m 45 "${file}" &

    # still < 3 max job -l ines? continue loop
    if [[ $(jobs|wc -l) -lt 3 ]]; then continue; fi

    # with 3 jobs, wait for last in line, then loop
    wait %%

done
# finish queue
wait
Enter fullscreen mode Exit fullscreen mode

To further develop the code, one could check for Bash version or alternative shells (zsh on MacOS) to switch code depending on context. I keep using these:

5) Bonus: One-liners

# sequential, slow
time ( for file in *.jpg; do jpegoptim -m 45 "${file}" ; done )

# concurrent, messy
time ( for file in *.jpg; do jpegoptim -m 45 "${file}" & done; wait )

# concurrent, fast/compatible
time ( for file in *.jpg; do jpegoptim -m 45 "${file}" & if [[ $(jobs|wc -l) -lt 3 ]]; then continue; fi; wait %%; done; wait )

# concurrent, fastest
time ( for file in *.jpg; do jpegoptim -m 45 "${file}" & if [[ $(jobs|wc -l) -lt 3 ]]; then continue; fi; wait -n; done; wait )
Enter fullscreen mode Exit fullscreen mode

Fun Fact

As the 20th birthday post by parallel author Ole Tange explains, the original version was leveraging make because it allows asynchronous processes as well.

concurrency Article's
30 articles in total
Favicon
Concurrency in C++: Mitigating Risks
Favicon
Poor man's parallel in Bash
Favicon
🚀 New Article Alert: Master sync.Pool in Golang! 🚀
Favicon
Overview of Lock API in java
Favicon
🚀 Mastering Concurrency in Go: A Deep Dive into sync.WaitGroup and sync.Cond
Favicon
🚀 Golang Goroutines: When Concurrency Meets Comedy! 🎭
Favicon
Concurrency vs Parallelism in Computing
Favicon
🚀 Demystifying Golang Concurrency: Channels and Select🚀
Favicon
Java Multithreading: Concurrency and Parallelism
Favicon
Asynchronous Python
Favicon
Why Modern Programming Isn't Always Asynchronous (And That's Okay, Mostly)
Favicon
Zero-Cost Abstractions in Rust: Asynchronous Programming Without Breaking a Sweat
Favicon
Navigating Concurrency for Large-Scale Systems
Favicon
Concurrency and Consistency: Juggling Multiple Users Without Missing a Beat
Favicon
Concurrency & Fault-tolerant In Distributed Systems
Favicon
Locking Mechanisms in High-Load Systems
Favicon
Process-based parallel execution of plain Minitest tests
Favicon
Concurrency in Python with Threading and Multiprocessing
Favicon
How to Use Goroutines for Concurrent Processing in Go
Favicon
Concurrency vs Parallelism
Favicon
Understanding Threading and Multiprocessing in Python: A Comprehensive Guide
Favicon
Writing Multi-threaded Applications in Java: A Comprehensive Guide
Favicon
🌐 Get started: MongoDB Change streams, Concurrency, backup snapshot & checkpoint, Compound Wildcard Indexes
Favicon
🌐 入门: MongoDB 更改流、并发、备份快照和检查点、复合通配符索引
Favicon
Total Madness #2: Async Locks
Favicon
Understanding Concurrency in React: A Guide to Smoother and More Responsive UIs
Favicon
Clear and Concise Concurrency with Coroutines in Kotlin
Favicon
Producer/Consumer (Produtor/Consumidor)
Favicon
Race Condition (Condição de Corrida)
Favicon
PHP HyperF -> Overlapping and Concurrency

Featured ones: