2026年7月11日
2026年7月11日
シェルスクリプトで並列処理を実装する方法(&とwait -n・xargs -P・GNU parallelの使い分け)
シェルスクリプトで処理を並列化するには、bash の & と wait で自前で制御する方法、xargs -P で同時実行数を指定する方法、GNU parallel を使う方法の3つが代表的です。少数のタスクなら & と wait、ファイル一覧などを決まった数ずつ処理するなら xargs -P、ログや失敗時の制御まで細かく扱いたいなら GNU parallel が向いています。この記事では、同時実行数の上限を守る書き方、失敗したジョブを取りこぼさない方法、出力が混ざる問題やCtrl+Cで止めたときの後始末まで、実運用でつまずきやすい点を中心に説明します。
なお、ループ内の外部コマンド呼び出しを減らすといった1本のスクリプト自体を速くする工夫は、別記事の「シェルスクリプトのパフォーマンス最適化」で扱います。この記事は「複数の処理を同時に走らせる」ことに絞っています。
並列化が効く処理・効かない処理
並列化で速くなるのは、待ち時間が多い処理や、CPU のコアを1つしか使っていない処理です。複数の URL への死活確認、複数サーバーへの rsync、画像の一括変換、ログファイルの圧縮などが該当します。
逆に、同じディスクに大量の書き込みをする処理や、1つのデータベースに重いクエリを投げる処理は、並列数を増やしても I/O やロックの待ちが増えるだけで、かえって遅くなることがあります。本番サーバーで動かす場合は、まず並列数 2 程度から始め、top や iostat で負荷を見ながら調整してください。CPU を使う処理の並列数は nproc で取得できるコア数が目安になります。
方法1:& と wait で並列実行する
最も基本的な形は、コマンドの末尾に & を付けてバックグラウンドで起動し、wait で全部の終了を待つ方法です。
#!/usr/bin/env bash
rsync -a /var/www/ backup1:/srv/www/ &
rsync -a /var/www/ backup2:/srv/www/ &
wait
echo "完了"
ただし、引数なしの wait は子プロセスがいくつ失敗しても終了ステータス 0 を返します。失敗を検知するには、起動したプロセスの PID を記録し、1つずつ wait PID で終了コードを受け取ります。
#!/usr/bin/env bash
pids=()
for host in backup1 backup2 backup3; do
rsync -a /var/www/ "$host:/srv/www/" &
pids+=("$!")
done
fail=0
for pid in "${pids[@]}"; do
wait "$pid" || fail=1
done
exit "$fail"
$! は直前にバックグラウンドで起動したプロセスの PID です。この書き方ならホストが3台でも10台でも、1台でも失敗すればスクリプト全体が 1 で終わります。
同時実行数に上限を付ける(wait -n)
対象が数百件あると、すべてを一度に起動するとサーバーの負荷が跳ね上がります。bash 4.3 以降なら、wait -n(いずれか1つのジョブの終了を待つ)を使って同時実行数を制限できます。
#!/usr/bin/env bash
set -uo pipefail
max_jobs=4
fail=0
compress_one() {
gzip -9 -- "$1"
}
for f in /var/log/app/*.log; do
[[ -e "$f" ]] || continue
# 実行中のジョブが上限に達していたら、1つ終わるまで待つ
while (( $(jobs -rp | wc -l) >= max_jobs )); do
wait -n || fail=1
done
compress_one "$f" &
done
# 残りのジョブを1つずつ回収して失敗を拾う
while (( $(jobs -rp | wc -l) > 0 )); do
wait -n || fail=1
done
exit "$fail"
jobs -rp は実行中のジョブの PID 一覧です。set -e を付けていないのは、wait -n が失敗ステータスを返した瞬間にスクリプトが止まり、残りのジョブを放置してしまうのを避けるためです。macOS に標準で入っている bash は 3.2 と古く wait -n が使えないため、Homebrew などで新しい bash を入れるか、後述の xargs -P を使ってください。bash のバージョンは bash --version で確認できます。
方法2:xargs -P で一覧を並列処理する
ファイルの一覧や URL の一覧を、決まった並列数で処理するなら xargs -P が手軽です。
find /var/www/html/wp-content/uploads -name '*.jpg' -print0 \
| xargs -0 -P 4 -n 1 jpegoptim --strip-all --quiet
-print0と-0: ファイル名の区切りを NUL 文字にし、空白や日本語を含むファイル名でも壊れないようにします。-P 4: 同時に4プロセスまで実行します。-n 1: 1回の実行に渡す引数を1つにします。
GNU xargs は、実行したコマンドのどれかが 1〜125 のステータスで終わると、自身は 123 で終了します。そのため if ! ... | xargs ...; then の形で失敗を検知できます。パイプラインの途中の失敗も拾うなら set -o pipefail を併用してください。
複数の処理を組み合わせたい場合は、関数を export -f して bash -c から呼び出す方法がよく使われます。
check_url() {
local code
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 "$1")
[[ "$code" == 200 ]] || { echo "NG $code $1"; return 1; }
}
export -f check_url
xargs -P 8 -n 1 bash -c 'check_url "$1"' _ < urls.txt
bash -c '...' _ の _ は $0 に入るダミーで、URL は $1 として関数に渡ります。bash -c "check_url $1" のように引数を文字列に埋め込むと、URL に含まれる記号がシェルに解釈されてしまうので避けてください。外部サイトに対して並列でリクエストする場合は、相手のサーバーに負荷をかけすぎない並列数にとどめることも大切です。
方法3:GNU parallel で失敗やログまで管理する
GNU parallel は並列実行専用のツールで、apt install parallel や dnf install parallel などで導入できます(2026年10月現在、RHEL 系では EPEL リポジトリが必要な場合があります)。オプションはバージョンによって追加・変更されることがあるため、手元の parallel --version と man parallel で確認してください。
parallel -j 4 --joblog /var/log/compress.joblog --halt soon,fail=1 \
gzip -9 -- {} ::: /var/log/app/*.log
-j 4: 同時実行数。-j 50%のようにコア数に対する割合も指定できます。--joblog: ジョブごとの開始時刻、所要時間、終了ステータスを記録します。後から失敗したジョブだけを--resume-failedで再実行するときにも使えます。--halt soon,fail=1: 1つでも失敗したら新しいジョブを起動せず、実行中のものが終わったら止めます。now,fail=1にすると実行中のジョブも止めます。
初回実行時に引用(citation)に関するメッセージが表示されることがあります。これは研究で使った場合の引用を求める案内で、表示を止める方法はメッセージ内で説明されています。対話的に確認したうえで、cron などで使う前に一度手動で実行しておくとよいでしょう。
出力が混ざる問題への対処
& や xargs -P で並列実行すると、複数のジョブの標準出力が行の途中で入り混じることがあります。ログとして読めなくなるのを防ぐには、次のいずれかの方法をとります。
- ジョブごとに別ファイルへ出力し、最後にまとめる(
cmd > "out/$name.log" 2>&1) - 1行ずつ完結した短いメッセージだけを出す
- GNU parallel を使う(既定でジョブごとに出力をまとめて表示します)
複数のジョブが同じファイルに追記したり、同じ設定ファイルを書き換えたりする場合は、flock で排他制御をしてください。並列化は「同じものを同時に触らない」設計にするのが基本です。
Ctrl+C や異常終了時の後始末
バックグラウンドで起動したジョブは、親のスクリプトが止まっても動き続けることがあります。途中で止めたときに子プロセスが残らないよう、trap で後始末を登録しておきます。
cleanup() {
local pids
pids=$(jobs -p)
if [[ -n "$pids" ]]; then
kill $pids 2>/dev/null
fi
}
trap 'cleanup; exit 130' INT TERM
trap cleanup EXIT
INT と TERM の trap で exit しているのは、trap を設定するとシグナルを受けてもスクリプトが自動では終了せず、処理が続いてしまうためです。
kill $pids を引用符で囲んでいないのは、複数の PID を別々の引数として渡すためです。中断したときに途中まで書かれたファイルが残る処理では、一時ファイルに書いてから mv で置き換える形にしておくと、壊れたファイルが残りません。
まとめ
- 少数のタスクは
&とwait PID、多数のタスクはwait -nかxargs -Pで同時実行数を制限する - 引数なしの
waitは失敗を返さないため、PID ごとに終了コードを回収する - ファイル名は
-print0とxargs -0で渡し、引数を文字列に埋め込まない - ログや再実行まで管理したいなら GNU parallel の
--joblogと--haltが便利 - 出力はジョブごとに分け、共有ファイルは
flockで守り、trapで子プロセスを後始末する - 並列数は小さく始めて、負荷を見ながら増やす