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 で子プロセスを後始末する
  • 並列数は小さく始めて、負荷を見ながら増やす

お気軽にご相談ください

お見積りへ お問い合わせへ
▲