GNU parallel 入門 - コマンドを並列実行して高速化する

GNU parallel 入門 - コマンドを並列実行して高速化する

この記事で解決できること

  • 複数コマンドを CPU コアぶん同時に走らせて 処理時間を短縮できる
  • xargs -P で困る 出力の混線・順序崩れ を回避できる
  • --joblog--resume中断した残りのジョブを続きから実行 できる

結論(使いどころ)

  • 1 件あたり重い処理を大量に回す(画像変換・ダウンロード・テスト)→ parallel
  • 出力を混ぜたくない / 入力順を保ちたい → -k
  • 中断した処理を続きから再開したい → --joblog + --resume

前提(対象環境)

  • OS: Ubuntu / Debian 系を想定(他ディストリも考え方は同じ)
  • GNU parallel(Ole Tange 作)が対象。moreutils 同梱の別物 parallel とは互換性がない

GNU parallel とは何か?

結論: 標準入力やコマンド引数のリストを受け取り、各要素に対してコマンドを並列に実行するツール。既定では CPU コア数ぶんのジョブを同時に走らせる。

GNU parallel は、xargs の「リストを受け取ってコマンドを組み立てる」発想を、並列実行と出力制御に特化させたコマンドである。基本形は次の 2 通り。

# 1) 引数を ::: の後ろに並べる
parallel echo ::: a b c

# 2) 標準入力から渡す
seq 1 3 | parallel echo
a
b
c

a b c のそれぞれに対して echo別プロセスとして同時に起動する。既定の同時実行数は CPU コア数なので、コア数より多い入力は順にスロットへ流し込まれる。

入力 1 件ごとにコマンドが 1 回起動する(xargs の既定とは逆)。1 回の起動で複数引数をまとめたい場合は後述の -N を使う。

インストールと最初の一歩はどうするのか?

結論: apt install parallel で導入。初回は学術引用の通知が出るので、parallel --citation を一度実行して了承を記録しておく。

sudo apt update
sudo apt install parallel
parallel --version

GNU parallel は学術論文での引用を求めるツールで、初回実行時に引用のお願いが表示される。CI やスクリプトで邪魔になる場合は、一度だけ次を実行して了承を記録する(~/.parallel/will-cite が作られ、以降は通知が消える)。

parallel --citation

ディストリによっては moreutils パッケージが別物の parallel を提供する。parallel --version で先頭行に GNU parallel と表示されるかを必ず確認する。表示されなければ GNU 版ではない。

なぜ xargs ではなく parallel なのか?

結論: xargs -P も並列実行できるが、出力が行単位で混ざる。parallel は出力をジョブ単位でまとめ、-k で入力順も保てる。

xargs -P 4 は手軽だが、複数ジョブの標準出力が1 行ずつ交互に混線しやすい。parallel は各ジョブの出力を内部でバッファし、ジョブが終わった単位でまとめて出力するため混ざらない。

観点 xargs -P parallel
出力の混線 起きやすい ジョブ単位で防ぐ
入力順の出力 保証なし -k で保証
置換文字列の柔軟さ {} のみ {} {.} {/}
実行ログ / 再開 なし --joblog --resume
進捗表示 なし --bar --eta

速度だけなら xargs -P で十分なことも多い。出力を壊したくない・再実行したいときに parallel の価値が出る。

置換文字列(プレースホルダ)はどう使うのか?

結論: {} が入力そのもの。{.} は拡張子除去、{/} はファイル名、{//} はディレクトリ、{#} はジョブ番号。出力名の組み立てに多用する。

入力をコマンドのどこに差し込むかは置換文字列で指定する。省略すると末尾に {} が補われる。

# *.wav を同名 .mp3 に変換({.} で拡張子を除去)
parallel ffmpeg -i {} {.}.mp3 ::: *.wav

主要な置換文字列は次のとおり。

記法 意味 例(入力 dir/file.txt
{} 入力そのもの dir/file.txt
{.} 拡張子を除去 dir/file
{/} ディレクトリを除いた名前 file.txt
{//} ディレクトリ部分 dir
{/.} 名前から拡張子も除去 file
{#} ジョブ通し番号 1, 2, ...
{%} ジョブスロット番号 1〜(同時実行数の範囲)
# 入力ごとにジョブ番号を添えて出力先を作る
parallel 'echo job {#}: {}' ::: alpha beta gamma
job 1: alpha
job 2: beta
job 3: gamma

ジョブ数と出力順はどう制御するのか?

結論: 同時実行数は -j で指定。-j0 は可能な限り多く、-j 200% はコア数の 2 倍。出力を入力順に揃えるなら -k を付ける。

# 同時 4 ジョブ
parallel -j 4 ./convert.sh ::: *.dat

# CPU コア数の 2 倍(I/O 待ちが多い処理向け)
parallel -j 200% curl -O ::: "${urls[@]}"

# 同時実行数の上限なし(注意して使う)
parallel -j0 echo ::: {1..100}

並列実行すると終わった順に出力されるため、入力と出力の対応が崩れる。入力順で出力したいときは -k--keep-order)を付ける。

seq 1 5 | parallel -k 'sleep $((RANDOM % 3)); echo {}'
1
2
3
4
5

本番投入前は --dry-run を付けると、実際に走らせるコマンド列だけを表示できる。置換文字列の展開ミスをここで潰す。

複数の入力を組み合わせるには?

結論: ::: を複数並べると全組み合わせ(直積)。--link で同じ位置どうしを対にする。1 行に複数列があるファイルは --colsep{1} {2} として使う。

# 直積: a-1 a-2 b-1 b-2 c-1 c-2 の 6 ジョブ
parallel echo ::: a b c ::: 1 2
# --link: a-1 b-2 c-3 のように位置で対応づけ
parallel --link echo ::: a b c ::: 1 2 3

ファイルを入力にするなら ::::(または -a)。CSV のように列を分けたい場合は --colsep

# hosts.txt の各行を引数にする
parallel ping -c1 {} :::: hosts.txt

# "user,host" 形式を列分割して使う
parallel --colsep ',' ssh {2} -l {1} uptime :::: targets.csv

進捗・ログ・失敗時の再実行はどうするのか?

結論: --bar で進捗バー、--joblog で各ジョブの結果を記録。--resume を併用すると、未完了ジョブだけを次回に引き継げる。

# 進捗バーを表示
parallel --bar ./task.sh ::: {1..50}

# 実行ログを記録(終了コード・所要時間が残る)
parallel --joblog run.log ./task.sh ::: *.dat

--joblog で残したログがあれば、--resumeすでに成功したジョブを飛ばして続きから実行できる。失敗したジョブだけやり直すなら --resume-failed

# 中断・失敗後、同じコマンドに --resume を足して再実行
parallel --joblog run.log --resume ./task.sh ::: *.dat

エラーで早めに止めたいときは --halt

# 1 つでも失敗したら走行中のジョブを終えて停止
parallel --halt now,fail=1 ./task.sh ::: *.dat

--resume同じ --joblog ファイルと同じコマンド が前提。コマンドや入力を変えると正しく再開できない。

標準入力を分割する --pipe とは?

結論: --pipe は引数ではなく標準入力そのものをブロックに分割し、各ブロックを並列のコマンドへ流す。巨大ログの集計などに向く。

これまでは「引数リストを並列化」していたが、--pipe1 本の入力ストリームを分割して並列処理する。

# 巨大ファイルを 10MB ずつに分け、各ブロックを並列で grep
cat huge.log | parallel --pipe --block 10M grep ERROR

--block で 1 ブロックのサイズを指定する。行の途中で切れないよう、parallel は改行境界で分割する。

実務でよく使うレシピ集

結論: 画像変換・一括ダウンロード・複数ホストへのコマンド実行が定番。--dry-run で確認してから本番投入するのが安全。

コピペ用テンプレ

# 画像を一括リサイズ(出力名は {.}_small.jpg)
parallel convert {} -resize 50% {.}_small.jpg ::: *.jpg

# URL 一覧を 8 並列でダウンロード
parallel -j8 wget -q ::: $(cat urls.txt)

# 複数ホストへ同じコマンド(出力はホスト単位でまとまる)
parallel -k --tag ssh {} 'uptime' :::: hosts.txt

# まず確認 → 問題なければ --dry-run を外す
parallel --dry-run ./batch.sh {} ::: input/*

--tag を付けると各出力行の先頭に入力(ホスト名など)が付き、どのジョブの結果かを追いやすくなる。

次に読む