wait – 子プロセスの終了を待つ/並列処理・PID指定・エラー対処まで解説

制御(停止・再開・終了)
スポンサーリンク

bash wait は、バックグラウンドで走る子プロセスの終了を正確に待つための基本コマンドです。

引数なしで「全部待つ」、$! で「直前だけ待つ」、wait <PID> で「特定だけ待つ」を使い分ければ、並列処理の完了判定やエラー検知が安定します。

この記事では基本構文から、for と組み合わせた並列制御、trap 連携、set -e の落とし穴、そして「waitが効かない」などのトラブル対処まで、実行例と戻り値の扱いに焦点を当てて解説します。

シェル操作が前提となるコマンドとなり、主にBashを前提とした解説を行います。

  1. 速攻まとめ – 結論
  2. waitコマンドの基本構文
  3. 主なオプション一覧
  4. waitコマンドの実行例
    1. 引数なし(すべての子プロセスを待つ)
    2. $! を使って直前のジョブを待つ
    3. PIDを直接指定して待つ
  5. waitで並列処理を制御する
    1. バックグラウンド実行とwaitの組み合わせ
    2. for文で複数ジョブを並列実行
    3. 並列数を制限する(セマフォ風制御)
    4. bash 5系以降の wait -n で「終わった順に処理」
  6. waitの応用テクニック
    1. trap と組み合わせて中断時に子プロセスを掃除する
    2. 戻り値で厳密に失敗検知する(集計 or 即中断)
    3. set -e と wait の関係(想定外の取りこぼしを防ぐ)
    4. wait -n で「終わった順に結果処理」
    5. subshell と & の取り扱い(親子関係を崩さない)
  7. よくあるエラーと対処法
    1. 「wait: pid … is not a child of this shell」
    2. 「バックグラウンド処理が待たれない/見失う」
    3. subshell(())と&の取り扱い
    4. wait -nが使えない
    5. set -eを有効にしているのに失敗を検知できない
    6. jobs・fg/bgとの混同
    7. nohup/disown後に待てない
    8. パイプライン/プロセス置換での取りこぼし
    9. すぐ使えるチェックリスト(最小限)
  8. 実用例:sleepとwaitの組み合わせ
  9. 関連コマンド・ツールとの比較
    1. ジョブ制御(jobs / fg / bg / disown)
    2. 終了とシグナル管理(kill / trap / timeout)
    3. 並列投入ユーティリティ(xargs -P / GNU parallel / make -j)
    4. 監視・存在確認(ps / kill -0 / pidfile)
    5. まとめ(選び方の軸)
  10. まとめ
    1. 結論
    2. 参考
  11. 関連記事

速攻まとめ – 結論

wait は「このシェルが起動した子プロセスの終了待ち」を行うコマンドです。

  • 引数なし: すべての子プロセスを待つ
  • wait <PID>: 指定PIDだけ待つ(子であることが前提)
  • wait $!: 直前にバックグラウンド実行したプロセスだけ待つ($!=直前の子PID)
  • 戻り値: 待機対象プロセスの終了ステータスを返す(異常終了の検知に使える)

waitコマンドの基本構文

wait コマンドは、現在のシェルから派生したバックグラウンドプロセス(子プロセス)が終了するまで処理を止めるコマンドです。
スクリプト中で非同期処理を扱うとき、「どのタイミングで待つか」を制御できるのが最大の利点です。

# POSIX 準拠(sh, bash, dash, ash など)
wait [ID...]

# Bash 拡張(例)
wait [-n] [-f] [-p VAR] [ID...]
  • IDプロセスID(PID) または ジョブ指定子(例: %1, %+, %-)。
  • 引数なしの wait は「このシェルが管理するすべてのバックグラウンドジョブ」が終了するまで待ちます。
  • 戻り値は、待った対象(最後に回収したもの)の終了ステータス。存在しない/他人のプロセスを指定すると一般に 127 を返します。

主なオプション一覧

オプション説明使用例
(なし)すべてのジョブの終了を待つwait
-n (bash)最初に終了した1つが終わるまで待つ(誰でもよい)wait -n
-f (bash)「停止(Stopped)」ではなく終了(Terminated)まで待つことを明確化wait -f %1
-p VAR (bash 5+)-n 等で待ち合わせた対象の PID/ジョブを変数に格納wait -n -p done_pid; echo "$done_pid $?"

シェルによりフラグは異なります。利用中のシェルでは help wait(bash)や man zshbuiltins を確認してください。
POSIX 仕様ではオプションは定義されておらず、ID... のみが引数です。

waitコマンドの実行例

引数なし(すべての子プロセスを待つ)

最もシンプルな使い方は引数なし。現在のシェルが起動したすべてのバックグラウンドジョブを待機します。

sleep 2 &
sleep 3 &
wait
echo "すべてのジョブが完了しました"

上記では sleep 2sleep 3 が並列に実行され、約3秒後に「すべてのジョブが完了しました」と出力されます。
どちらか一方が遅れても、wait は両方の完了を確認するまで次に進みません。

$! を使って直前のジョブを待つ

$! は「直前にバックグラウンド実行したプロセスのPID(プロセスID)」を保持します。
これを wait に渡すことで、特定のジョブだけを待つことが可能です。

long_task &
wait $!
echo "直前のジョブが完了しました"

他の処理を並行して実行したい場合でも、個別のジョブだけを正確に待てる点が便利です。

PIDを直接指定して待つ

既にPIDを取得している場合は、数値を直接指定することもできます。

some_process &
pid=$!
# 途中で別処理
wait "$pid"
echo "PID ${pid} の処理が完了しました"

PID指定では戻り値(終了コード)も受け取れるため、成功・失敗の判定にも利用できます。

command & pid=$!
wait "$pid"
echo "終了コード: $?"

このように、wait の戻り値は「待ったプロセスの終了ステータス」を返すため、エラーハンドリングに直結します。
set -e が効かない場面でも、wait の戻り値で安全に失敗検知ができます。

waitで並列処理を制御する

wait は、複数の処理を同時に実行してから「すべて終わるまで待つ」ときに真価を発揮します。
&(バックグラウンド実行)と組み合わせることで、処理時間の短縮効率的なCPU利用が可能になります。

バックグラウンド実行とwaitの組み合わせ

次の例では、3つの sleep コマンドを同時に実行し、すべてが完了するまで待ちます。

sleep 3 &
sleep 2 &
sleep 1 &
wait
echo "すべて完了しました"

結果として、合計時間は「最も長いsleep(3秒)」と同じになります。
これにより、複数の処理を並行して走らせながら、最後に1回だけ同期を取ることができます。

for文で複数ジョブを並列実行

for と組み合わせることで、一定のパターンで並列処理を行うことも簡単です。

for i in 1 2 3 4 5; do
  (echo "処理$i開始"; sleep $i; echo "処理$i終了") &
done
wait
echo "全処理が終了しました"

このように書くと、5つのジョブが同時に開始され、すべてが完了した時点で次の行へ進みます。

並列数を制限する(セマフォ風制御)

同時実行数を制御したい場合は、配列でPIDを管理して、一定数を超えたら wait を挟む方法が便利です。

max=3
pids=()

for n in {1..8}; do
  (echo "Job $n start"; sleep $((RANDOM % 3 + 1)); echo "Job $n done") &
  pids+=($!)
  if (( ${#pids[@]} >= max )); then
    wait "${pids[0]}"
    pids=("${pids[@]:1}")
  fi
done

for p in "${pids[@]}"; do
  wait "$p"
done
echo "すべてのジョブが完了しました"

これにより、常に最大3つのジョブだけが同時に実行されるようになります。
wait は「ブロッキング型」なので、並列実行数を柔軟に制御できます。

bash 5系以降の wait -n で「終わった順に処理」

Bash 5以降では、wait -n オプションを使って「どれか1つが終わるたびに戻る」動作も可能です。

for s in 3 1 2; do sleep "$s" & done
while wait -n; do
  echo "1つのジョブが完了しました"
done
echo "すべて完了"

この方法を使えば、「最初に終わったジョブから順に結果処理を行う」といった高度な並列制御も実現できます。

waitの応用テクニック

wait は「並列 → 同期」の基本に加えて、戻り値の活用シグナル処理(trap)と組み合わせることで実運用に耐える堅牢なスクリプトになります。特にCI/CDや長時間バッチでは、失敗検知・中断時の後片付け・部分再実行の設計が重要です。

trap と組み合わせて中断時に子プロセスを掃除する

Ctrl-C(SIGINT)や停止要求(SIGTERM)で中断されたとき、放置された子プロセスが残るとリソース食いの原因になります。trap でシグナルを受けたら子をまとめて終了し、wait で回収するのが安全です。

#!/usr/bin/env bash
set -euo pipefail
pids=()

cleanup() {
  for p in "${pids[@]}"; do
    kill -TERM "$p" 2>/dev/null || true
  done
  wait || true    # 終了コードは無視してゾンビ化を防止
}
trap cleanup INT TERM

for u in url1 url2 url3; do
  curl -fsS "$u" -o "out/$(basename "$u")" & pids+=($!)
done

# いずれかが失敗したら即中断したい場合
for p in "${pids[@]}"; do
  wait "$p" || { echo "失敗: $p"; exit 1; }
done
echo "全件成功"

戻り値で厳密に失敗検知する(集計 or 即中断)

バックグラウンドの失敗は勝手に伝播しません。wait の終了コードで判断し、要件に応じて「一つでも失敗で中断」か「全体の成功数・失敗数を集計」かを選びます。

# 一つでも失敗で中断(fail-fast)
fails=0
for p in "${pids[@]}"; do
  if ! wait "$p"; then
    ((fails++))
    echo "失敗: $p"
    exit 1
  fi
done
# 成功/失敗を集計(レポート用)
ok=0; ng=0
for p in "${pids[@]}"; do
  if wait "$p"; then ((ok++)); else ((ng++)); fi
done
printf '成功:%d 失敗:%d\n' "$ok" "$ng"
exit $(( ng > 0 ))

set -e と wait の関係(想定外の取りこぼしを防ぐ)

set -eバックグラウンドの失敗を検出しません。したがって、並列区間のエラー検知は必ず wait で行います。パイプやプロセス置換の中も含め、「失敗検知の責務はwait」と覚えておくと事故が減ります。

set -euo pipefail
build & p1=$!
test_suite & p2=$!

# ここで明示的にチェック
wait "$p1" || { echo "build失敗"; exit 1; }
wait "$p2" || { echo "test失敗";  exit 1; }

wait -n で「終わった順に結果処理」

Bash 5系以降なら wait -n。長短が混在するジョブ群で、終わったものから結果処理すると滞留が減ります。

set -euo pipefail
for t in 5 1 3; do (sleep "$t"; echo "done:$t") & done
while wait -n; do
  # ここに「終わったジョブの後処理」を書く(ログ収集・一時ファイル削除など)
  :
done

subshell と & の取り扱い(親子関係を崩さない)

( … )subshell を作ります。subshell 内で & した子は親シェルからは子でないため、親で wait できない場合があります。親から待ちたい子は親シェルで & を起動し、$! を直後に変数へ保存しましょう。

# 悪い例(親から待てない可能性)
( long_task & )

# 良い例(親の子として待てる)
long_task & pid=$!
wait "$pid"

この章のポイントは、「待つ対象を自分の子として起動する」「戻り値で必ず判定する」「中断時に必ず掃除する」の3点です。運用トラブルはほぼここで防げます。

よくあるエラーと対処法

wait が意図どおりに働かない多くの原因は、「子プロセスかどうか」「PIDの受け渡し」「subshellの混入」にあります。典型例と直し方を押さえておくと、実運用での詰まりを避けられます。

「wait: pid … is not a child of this shell」

原因:そのPIDは現在のシェルの“子”ではありません(別ターミナルで起動/nohupdisown後/subshell内で生成 など)。
対処同じシェルから起動し、直後に$!を保存して親スコープでwaitします。

# NG: subshell内で生成 → 親からは待てない
( long_task & )

# OK: 親の子として起動 → $! をすぐ保存
long_task & pid=$!
wait "$pid"

「バックグラウンド処理が待たれない/見失う」

原因$!をすぐ変数化していない、関数や別スコープでPIDを失っている。
対処起動直後にPIDを配列に格納し、最後にまとめてwait

pids=()
for f in *.jpg; do
  convert "$f" "out/$f" & pids+=($!)
done
for p in "${pids[@]}"; do
  wait "$p" || { echo "失敗:$p"; exit 1; }
done

subshell(())と&の取り扱い

症状:親でwaitしても完了しない/PIDが別階層。
対処必要がない限り()で囲まず、親シェルで&を起動する。どうしても()が必要なら、subshellの中でwaitを完結させる。

# subshell内で完結させる例
( task1 & task2 & wait )

wait -nが使えない

原因:Bash 5未満では-n非対応。
対処:最小PIDを待つ/配列から1つずつwaitする等で代替。可能ならBash更新を検討。

# 代替:先着のPID順にwait(厳密な“完了順”ではない)
for p in "${pids[@]}"; do
  wait "$p" || exit 1
done

set -eを有効にしているのに失敗を検知できない

原因バックグラウンドの失敗はset -eの対象外
対処:各waitの戻り値($?)で必ず判定し、必要なら即exit 1

cmdA & p1=$!
cmdB & p2=$!
wait "$p1" || { echo "A失敗"; exit 1; }
wait "$p2" || { echo "B失敗"; exit 1; }

jobs・fg/bgとの混同

症状:ジョブ番号(%1など)とPIDの混在で混乱。
対処waitPIDで統一。ジョブ制御を使うならjobs -pでPID化。

sleep 5 & sleep 3 &
for pid in $(jobs -p); do
  wait "$pid"
done

nohup/disown後に待てない

原因:親子関係が切れて“孤児化”している。
対処孤児化前に完了させるか、状態確認は別手段(pidfile+kill -0waitの代わりにポーリング)。

# pidfileで生存確認の例
my_daemon & echo $! > /tmp/my_daemon.pid
# (以後はwait不可。監視は kill -0 "$(cat /tmp/my_daemon.pid)" などで)

パイプライン/プロセス置換での取りこぼし

症状cmd1 | cmd2 の内部失敗が見落とされる。
対処set -o pipefailを有効化し、並列化した部分はwaitで明示判定。プロセス置換 < <() 内の失敗も伝播しにくい点に注意。

set -euo pipefail
# 並列区間の失敗は wait で拾う
build & p1=$!; test & p2=$!
wait "$p1" && wait "$p2"

すぐ使えるチェックリスト(最小限)

  • 起動直後に$!を保存したか
  • 親の子として起動しているか(()で囲っていないか)
  • 戻り値で判定しているか(set -eに過信なし)
  • 必要ならtrapで掃除しているか(中断時の孤児対策)

子であること/PIDを握ること/戻り値で見ること」の3点を守れば、waitのトラブルはほぼ防げます。

実用例:sleepとwaitの組み合わせ

bash wait の挙動は sleep を使うと可視化しやすく、並列の効き方や「どこで待つか」による全体時間の違いが一目で分かります。まずは3つの処理を同時に走らせ、最後に1回だけ wait する基本形です。

# 3つを並列 → 最後に1回だけ同期
time bash -c '
  sleep 1 &
  sleep 2 &
  sleep 3 &
  wait
'

出力(例):

real    0m3.00s

最も長い sleep 3 に支配され、約3秒で終了します。個別に wait しても総時間は同じですが、戻り値の取得と失敗検知が可能になります。

# 個別待ちで戻り値を確認
time bash -c '
  sleep 1 & p1=$!
  (sleep 2; exit 0) & p2=$!
  (sleep 3; exit 7) & p3=$!

  ok=0; ng=0
  wait "$p1" && ((ok++)) || ((ng++))
  wait "$p2" && ((ok++)) || ((ng++))
  wait "$p3" && ((ok++)) || ((ng++))
  echo "成功:$ok 失敗:$ng"
'

出力(例):

成功:2 失敗:1
real    0m3.00s

終了コードの収集まで一括で行えるため、CI/CDやバッチでの合否判定に直結します。

さらに、並列度を制限するとCPUやI/Oの混雑を回避できます。次は常に2本まで同時実行する例です。

# 常時2並列(セマフォ風)
time bash -c '
  set -euo pipefail
  max=2
  pids=()
  for s in 3 1 2 2 1; do
    (sleep "$s") & pids+=($!)
    if (( ${#pids[@]} >= max )); then
      wait "${pids[0]}"; pids=("${pids[@]:1}")
    fi
  done
  for p in "${pids[@]}"; do wait "$p"; done
'

最後に、Bash 5系の wait -n を使うと「終わった順」に後処理を差し込めます。ログ転送や一時ファイルの削除を滞留なく進めたいときに有効です。

# 終わった順に処理
bash -c '
  for s in 3 1 2; do (sleep "$s"; echo "done:$s") & done
  while wait -n; do
    # 終了したジョブに対する後処理を書く
    :
  done
'

sleep と組み合わせた最小実験で、「全部待つ」「個別に待つ」「制限して待つ」「終わった順に待つ」の違いを把握しておくと、実務スクリプトの設計が格段に安定します。

関連コマンド・ツールとの比較

wait は「自分が起動した子プロセスの終了待ち」に特化しています。混同しがちな周辺機能との役割分担を押さえると、正しい道具選びができます。

ジョブ制御(jobs / fg / bg / disown)

  • 目的:対話シェルでの前面/背面切り替えや一覧表示。
  • 違い:waitPIDを待つのに対し、ジョブ制御は操作対象の切替が主。スクリプトでは基本使わず、jobs -pで**PID化してからwait**が安全。
sleep 5 & sleep 3 &
for pid in $(jobs -p); do
  wait "$pid"
done

disown すると親子関係が切れ、waitできなくなる点に注意。

終了とシグナル管理(kill / trap / timeout)

  • 目的:プロセスの終了指示kill)、中断時の後片付けtrap)、タイムアウト強制timeout)。
  • 使い分け:wait自然終了を待つ。強制終了やタイムリミットは別途組み合わせる。
# タイムアウトを付けた並列
pids=()
for t in 1 5 10; do
  timeout 3s bash -c "sleep $t" & pids+=($!)
done
for p in "${pids[@]}"; do
  wait "$p" || echo "タイムアウト/失敗: $p"
done

並列投入ユーティリティ(xargs -P / GNU parallel / make -j)

  • 目的:大量タスクを制御された並列度で投入し、失敗管理や再実行を楽にする。
  • 使い分け:軽量スクリプトはBash+waitで十分。多数/重め/再実行前提なら専用ツールが有利。
# xargs -P:標準的・軽量
printf '%s\n' *.jpg | xargs -P4 -I{} convert {} -resize 50% "out/{}"

# GNU parallel:進捗/失敗収集/置換が強力(要インストール)
parallel -j4 convert {} -resize 50% out/{} ::: *.jpg

監視・存在確認(ps / kill -0 / pidfile)

  • 目的:自分の子でないプロセスの生存確認や外部プロセス監視。
  • 使い分け:wait子限定。他者プロセスは kill -0 <PID>(信号送らず存在確認)やpidfile運用で監視。
if kill -0 "$pid" 2>/dev/null; then
  echo "まだ動いている"
else
  echo "終了済み"
fi

まとめ(選び方の軸)

  • 自分の子を待つwait(基本)
  • タイムアウトさせたいtimeoutwait
  • 大量並列・失敗制御・再実行xargs -P / GNU parallel
  • 中断時の掃除trapkillwait
  • 他プロセス監視kill -0 / pidfile(waitの守備範囲外)

この住み分けを押さえると、wait小さく正確な同期原語として使い、必要に応じて周辺ツールで拡張できます。

まとめ

bash wait はシンプルながら、並列処理を安定化させる鍵となるコマンドです。
引数なし・PID指定・$!wait -n などの基本パターンを理解するだけで、スクリプトの安全性と効率が大きく向上します。

for文との組み合わせsleepコマンドの基礎 を確認すれば、実際にどのタイミングで wait を使うべきかが感覚的につかめるはずです。
また、traptrapコマンドの使い方 を併用すれば、実行中断やエラー時の「後始末」も自動化できます。

より高度な並列処理やエラー制御を行いたい場合は、xargs -P や GNU parallel の活用まとめ を参考にしてください。
これらを組み合わせれば、開発・テスト・CI/CD のどの段階でも「失敗を見落とさず、全体を待つ」安全な処理設計が実現できます。

結論

  • wait は「子プロセスの終了を待つ」ための最も基本的な同期手段
  • $! を正しく使えば、個別ジョブ管理やエラーハンドリングも容易
  • trap と組み合わせれば、安全で中断に強いスクリプトが書ける
  • 複数並列を扱うなら for + wait または xargs -P / parallel が最適

一言で言うなら:

“Bashのwaitは、シンプルだけど信頼できる同期の基本構文”。

参考

スポンサーリンク
Bash玄

はじめまして!Bash玄です。

エンジニアとしてシステム運用に携わる中で、手作業の多さに限界を感じ、Bashスクリプトを活用して業務を効率化したのがきっかけで、この道に入りました。「手作業は負け」「スクリプトはシンプルに」をモットーに、誰でも実践できるBashスクリプトの書き方を発信しています。

このサイトでは、Bashの基礎から実践的なスクリプト作成まで、初心者でもわかりやすく解説しています。少しでも「Bashって便利だな」と思ってもらえたら嬉しいです!

# 好きなこと
- シンプルなコードを書くこと
- コマンドラインを快適にカスタマイズすること
- 自動化で時間を生み出すこと

# このサイトを読んでほしい人
- Bashに興味があるけど、何から始めればいいかわからない人
- 定型業務を自動化したい人
- 効率よくターミナルを使いこなしたい人

Bashの世界に一歩踏み出して、一緒に「Bash道」を極めていきましょう!

Bash玄をフォローする

コメント