
2026/11/14 2:13
並列シェルコマンドキュー
RSS: https://news.ycombinator.com/rss
要約▶
日本語訳:
改善は不要です;既存の要約は主要ポイントを完全に網羅している上で、正確かつ明確です。
本文
Bash で並列実行キューを制限する実装方法
Bash の処理速度向上を図る一方で、並列度(コンカレンシー)を抑制したいというユースケースにおいて、スレッドプールのような仕組みを作成する方法について解説します。
本手法は Bash と Zsh に対応し、堅牢性と互換性を確保するためには一定の工夫が必要ですが、以下のコードをスクリプトや設定ファイル(
.bashrc / .zshrc)に追加することで利用可能です。
コード概要
以下は、シェルコマンドを実行するための**命令列(キュー)**を作成する関数の実装です。 事前にすべてのコマンドを用意する必要はなく、時間をかけて順次キューに追加していくことができます。
# シェルのコマンドキューを開始します。 # @param {number} $1 一度に行う最大のジョブ数 (並列度) queue_start() { # xargs による並列性の限界として有効な値であることを検証する if [[ ! ${1:-} =~ ^[1-9][0-9]*$ ]]; then echo "queue_start: size must be a positive integer, got '${1:-}'" >&2 return 1 fi # 継承または以前のコマンドキューから受け継がれたファイルディスクリプター(FD)を閉じる if [[ -n ${_queue_fd:-} ]]; then eval "exec ${_queue_fd}>&-" unset _queue_fd _queue_pid fi # zsh のリダイレクト上限である 3 から 9 までの範囲で、使用されていない最初の FD を探す local fd=3 while { : <&"${fd}"; } 2>/dev/null; do fd=$(( fd + 1 )); done if [[ ${fd} -gt 9 ]]; then echo "queue_start: no free fd from 3 to 9" >&2 return 1 fi # プライベートな一時ディレクトリに FIFO キューファイルを作成する local dir dir=$(mktemp -d "${TMPDIR:-/tmp}/queue.XXXXXX") mkfifo "${dir}/queue" # コマンドを実行するシェルを見つける(現在のシェル) local shell shell=$(ps -o comm= -p "$") shell=${shell#-} # xargs をバックグラウンドで開始し、キューファイルから NULL で区切られたコマンドを読み取る。 # ジョブメッセージはジョブ制御をオフにし、ブロックの標準エラー出力を捨てて隠す。xargs は、 # ファイルが開かれるまでの間は、キューファイルの fd コピーを通じて本当の標準エラー出力を受け取る。 eval "exec ${fd}>&2" local opts=$- set +m { # shellcheck disable=SC2016 xargs -0 -n 1 -P "$1" "${shell}" -c ' # コマンドの出力はストリーミングではなくキャプチャされるため、コマンドの出力が適切に分離される out=$(eval -- "$1" </dev/null 2>&1); rc=$? [ -z "${out}" ] || printf "%s\n" "${out}" # xargs を止めるような 255 というコードになるのを防ぐため、非ゼロのエラーコードをすべて 1 に変換する [ "${rc}" -eq 0 ] || { printf "exit %d: %s\n" "${rc}" "$1" >&2; exit 1; }' _ <"${dir}/queue" 2>&"${fd}" & _queue_pid=$! disown || : } 2>/dev/null if [[ ${opts} == *m* ]]; then set -m; fi # xargs が読み書きのために開くのを待機しながら、キューファイルを書き込み用に開く。 # 両端が開かれている状態であれば、ファイルを安全に削除できる。 eval "exec ${fd}>\"\${dir}/queue\"" _queue_fd=${fd} rm -rf "${dir}" # この PID の子プロセスを終了するための exit ハンドラを登録する。これにより zsh は bash と同様に振る舞うようにする。 setopt localoptions posixtraps 2>/dev/null || : trap _queue_stop EXIT if [[ $- != *i* ]]; then trap 'exit 129' HUP trap 'exit 130' INT trap 'exit 143' TERM fi } # シェルのコマンドキューにコマンドをキューします。 # @param {string} $1 実行するコマンド queue_submit() { if [[ -z ${_queue_fd:-} ]]; then echo "queue_submit: no command queue started in this shell" >&2 return 1 fi # キューファイルにコマンドを書き込む(キューがいっぱいだった場合はブロック)。 # xargs が終了してしまった場合でも、このシェルのスクリプトが殺されるのではなく printf プロセスが殺されるようになっています。 if [[ $- == *i* ]]; then # 対話型シェルは TERM/HUP/INT トラップを保持しないため、キューが満杯になったことがこれらのトラップが遅延することを避けるために、 # foreground でコマンドを書き込む(対話型シェルの場合は foreground で行う必要がある)。 env printf '%s\0' "$1" >&"${_queue_fd}" else # キューが満杯になっても TERM/HUP/INT トラップが遅延しないように、バックグラウンドでコマンドを書き込む。 env printf '%s\0' "$1" >&"${_queue_fd}" & wait "$!" fi || { echo "queue_submit: couldn't queue the command; the command queue may have stopped" >&2 return 1 } } # キューファイルを閉じ、xargs が実行中のジョブを完了するのを待つ。その後 xargs が終了するのを待ちます。 queue_wait() { if [[ -z ${_queue_fd:-} ]]; then echo "queue_wait: no command queue started in this shell" >&2 return 1 fi eval "exec ${_queue_fd}>&-" while kill -0 "${_queue_pid}" 2>/dev/null; do sleep 0.1; done unset _queue_fd _queue_pid } # 指定された PID 以下のすべてのプロセスを停止します。 # @param {number=} $1 停止すべき子孫を持つ親の PID、デフォルトは現在のシェルの PID _queue_stop() { # subshell の場合は $ が親プロセスの PID であるため、子の PPID からこのシェルの PID を取得し、 # デフォルトとしてこのシェルの PID を採用する。 local parent=${1:-$(exec sh -c 'echo "${PPID}"')} local pid for pid in $(exec pgrep -P "${parent}"); do # プロセスを一時停止させ、新しい子プロセスを開始できなくする。 kill -STOP "${pid}" 2>/dev/null || continue # プロセスの子孫を終了させ、その後プロセス自体を終了させる。 _queue_stop "${pid}" kill -TERM "${pid}" 2>/dev/null || : # 終了信号に対応できるように、プロセスを再開する。 kill -CONT "${pid}" 2>/dev/null || : done } # exit ですべてのバックグラウンドプロセスを停止する。それらは Ctrl-C を無視し、TERM シグナルも受け取らないためです。 if [[ $- != *i* ]]; then trap _queue_stop EXIT trap 'exit 129' HUP trap 'exit 130' INT trap 'exit 143' TERM fi
動作仕組みの要約
: キュー(FIFO ファイルで管理)を初期化し、実行コンポーネント(queue_start
で並列度を制御)を開始します。xargs -P
: コマンドをキューに追加します。queue_submit
: キューが空になるまで待機し、リソースのクリーンアップを行います。呼び出しは任意ですが、終了時に推奨されます。queue_wait
使用例
大規模な Web リクエストの実行(Postman Echo)
最大 3 並列で 5 件のリクエストを実行します。
$ queue_start 3 $ for n in 1 2 3 4 5; do queue_submit "set -o pipefail; \ curl -sS --fail-with-body --max-time 10 -X POST -H 'Content-Type: application/json' \ -d '{\"request\": ${n}}' \ https://postman-echo.com/post | \ jq -c .json" done $ queue_wait {"request":1} {"request":2} {"request":3} {"request":4} {"request":5}
並列性の確認
最大 2 並列で指定された順序に従って実行されることを示します。コマンド 2 が完了するまで、コマンド 3 は開始されません。
$ queue_start 2 $ queue_submit 'sleep 1; echo first' $ queue_submit 'echo second' $ queue_submit 'echo third' $ queue_wait second third first
サブシェルによる継承と隔離
サブシェルではそれぞれ独立したコマンドキューが機能します。
# 共通のキューから両方のシェルへコマンドを送信 ( queue_start 2 ) & queue_submit 'echo "main shell job"' wait sleep 0.1 # 子プロセスが終了するのを待つ # 別々のキューで動作確認 first=$! ( queue_start 2 for n in 1 2 3 4; do queue_submit "sleep 1; echo first queue, job ${n}"; done queue_wait ) & wait "${first}" second=$! ( queue_start 1 for n in 1 2; do queue_submit "sleep 1; echo second queue, job ${n}"; done queue_wait ) & wait "${second}" # 出力結果:各キューが独立して動作していることが確認できる second queue, job 1 first queue, job 1 first queue, job 2 second queue, job 2 first queue, job 3 first queue, job 4
動的なコマンド生成(出力の各行をキューに追加)
他のコマンドの実行結果を、それぞれ個別のコマンドとしてキューへ投入できます。
$ queue_start 2 $ while IFS= read -r line; do queue_submit "${line}" done < <(printf 'echo "from command output, line %d"\n' 1 2 3) $ queue_wait from command output, line 1 from command output, line 2 from command output, line 3
ファイルの各行をキューに追加
テキストファイルから読み込んだ各行をコマンドとして実行します。
$ cat commands.txt echo "cmd 1" echo "cmd 2" echo "cmd 3" $ queue_start 2 $ while IFS= read -r line; do queue_submit "${line}" done < commands.txt $ queue_wait
既知の問題点と限界
Bash/Zsh で実装する以上、以下の制限や課題があります。Python など他の言語を使う方が適しているケースも多いため注意してください。
- キューの単一性: シェルには一つのコマンドキューしか保持できません。複雑なマルチキューを実現するには多くの追加コードが必要ですが、通常は不要です(サブシェルで隔離する場合は可能)。
- 出力の分離保証がない: 意図的にストリーミングを行いません。複数のコマンドが同じ出力を生成する場合、混在して表示される可能性があります(
より大きな出力は特に注意)。PIPE_BUF - エラーハンドリングの簡略化:
はジョブ失敗の詳細なステータス値を返しません。エラー検知には一時ファイルや追加の FD が必要となり複雑化します。queue_wait - プロセス終了時の遅延:
は開始後のプロセスも待機しますが、書き込み FD が継承されるため、すべてのコピーが閉じるまでキューの終端が見えない場合があります。queue_wait - 非選択的なプロセス停止:
は子孫を indiscriminately(非選択的に)停止するため、_queue_stop
以前に開始されたバックグラウンドジョブも終了してしまいます。queue_start
なぜ単に xargs -P
を使わないのか?
xargs -P事前に処理すべきコマンドのリストが既にある場合は、以下の方法でシンプルに並列実行可能です。
# コマンドをヌル文字区切りで出力し、xargs に渡す $ printf 'echo "from command output, line %d"\0' 1 2 3 | \ xargs -0 -n 1 -P 2 bash -c # またはテキストファイルから読み取る $ tr '\n' '\0' < commands.txt | \ xargs -0 -n 1 -P 2 bash -c
より高度な並列処理を必要とする場合は、macOS では標準搭載されていませんが
が推奨されます。GNU parallel
$ printf 'echo "from command output, line %d"\n' 1 2 3 | \ parallel -j 2