この記事の狙い
並列実行される Bash スクリプトで二重起動や同時書き込みを防ぐためのロック設計をまとめます。flock を中心に、PID ファイル、一時ディレクトリロック(mkdir 原子性)、タイムアウト/再試行、**掃除(trap)**まで、実務で使える最小実装を提示します。
前提と対象
- Bash 4+(
set -Eeuo pipefail推奨) - Linux(
flock(1)は util-linux 由来)。macOS ではflockが無い環境があるため、代替としてmkdirロックや Homebrew 導入を検討
TL;DR(最小実装・コピペ可)
#!/usr/bin/env bash
set -Eeuo pipefail
# 単一実行ロック(排他・タイムアウト10秒)
lock_file="/tmp/myjob.lock"
exec {lfd}>"$lock_file"
if ! flock -x -w 10 "$lfd"; then
echo "another instance running" >&2
exit 0 # 競合は非エラー扱いでもよい
fi
trap 'flock -u "$lfd"; rm -f "$lock_file"' EXIT
# クリティカルセクション
do_work
ポイント
- ファイル記述子に対する flock が堅い(
exec {fd}>"file"→flock "$fd") trap ... EXITで必ず解放&掃除- タイムアウトは
-w SEC、非ブロックは-n
ロック方式の選択指針(どれを使う?)
| 方式 | 強み | 注意点 | 典型用途 |
|---|---|---|---|
flock(排他/共有) | シンプル・高速・FD連動 | 一部OSにない/NFSは非推奨 | 単一実行、ファイル更新の直列化 |
PIDファイル(noclobber) | 依存コマンド少 | 陳腐化対応が必要 | デーモン化・長期ジョブ |
mkdir ロック(lockdir) | NFSでも比較的安全 | 片付け必須 | ディレクトリ単位の排他 |
原子的書き換え(tmp→mv) | 競合を無視できる | 同期は別途必要 | 出力の完成性保証 |
flock パターン集
A) ファイル記述子でロック(推奨)
lock() {
local path="$1" mode="${2:-exclusive}" wait="${3:-0}" fd
exec {fd}>"$path"
case "$mode" in
exclusive) flock -x ${wait:+-w "$wait"} "$fd" ;;
shared) flock -s ${wait:+-w "$wait"} "$fd" ;;
esac || return 1
echo "$fd" # 返すFD番号を呼び元が保持
}
# 使用例
lfd="$(lock /tmp/report.lock exclusive 5)" || { echo "busy" >&2; exit 0; }
trap 'flock -u "$lfd"; rm -f /tmp/report.lock' EXIT
B) コマンド/ブロックに一時的にかける
# ファイルを開かず、ファイルパスに対してコマンドを包む
flock -x -w 10 /tmp/my.lock -- some_command --arg
# ブロックにかけたい(サブシェルで一括)
flock -x /tmp/my.lock -- bash -c '
set -Eeuo pipefail
critical_section
'
C) 共有ロック(読み取り側)
# ライターは -x、リーダーは -s として両立
exec {rfd}<>"/tmp/cache.lock"
flock -s "$rfd"
read_only_query
flock -u "$rfd"
D) キー別ロック(レコード単位の直列化)
key="$1"
hash="$(printf '%s' "$key" | sha1sum | cut -c1-16)"
lock="/tmp/myapp.$hash.lock"
exec {lfd}>"$lock"; flock -x "$lfd"
# key に紐づく処理
PID ファイル方式(シンプルだが陳腐化に注意)
E) noclobber を使った単一実行
pidfile="/tmp/myjob.pid"
umask 077
if ( set -o noclobber; : >"$pidfile" ) 2>/dev/null; then
printf '%s\n' "$$" >"$pidfile"
trap 'rm -f "$pidfile"' EXIT
else
echo "running? pid=$(cat "$pidfile" 2>/dev/null || echo '?')" >&2
exit 0
fi
noclobberにより既存ファイル上書きを防止(原子的作成)- 陳腐化対策:PID の生存確認を入れるとより堅牢
if kill -0 "$(cat "$pidfile")" 2>/dev/null; then
echo "still alive"; exit 0
else
echo "stale pidfile; removing" >&2
rm -f "$pidfile"
fi
mkdir ロック(NFS でも効きやすい)
F) ディレクトリ作成の原子性を利用
lockdir="/tmp/my.lockdir"
if mkdir "$lockdir" 2>/dev/null; then
trap 'r=$?; rm -rf "$lockdir"; exit $r' EXIT
else
echo "busy" >&2; exit 0
fi
# クリティカルセクション
mkdirは存在しないときのみ成功(原子的)- NFS 環境では
flockよりもこちらが有効な場合が多い - 障害終了で残骸が残るため、期限付きクリーンを設けると良い
# 5分以上前の lockdir は陳腐とみなす例
[ -d "$lockdir" ] && find "$lockdir" -maxdepth 0 -mmin +5 -exec rm -rf {} +
タイムアウト・再試行・バックオフ
G) 汎用リトライヘルパ
retry_with_backoff() { # cmd...
local tries="${TRIES:-5}" base="${BASE:-0.2}" i
for ((i=1;i<=tries;i++)); do
"$@" && return 0
sleep "$(awk -v i="$i" -v b="$base" 'BEGIN{printf "%.2f", b*(2^(i-1))}')"
done
return 1
}
# ロック取得をリトライ
retry_with_backoff flock -n /tmp/q.lock -- true || { echo "busy"; exit 0; }
原子的書き換え(完成性の保証)
H) 書いてから mv(同一ファイルシステム内)
out="/var/app/data.json"
tmp="$(mktemp "${out}.XXXXXX")"
trap 'rm -f "$tmp"' RETURN
generate_json >"$tmp"
mv -f "$tmp" "$out" # rename は原子的(同一FS)
trap - RETURN
- ロックと組み合わせれば汚れた中間ファイルを他プロセスが読まない
並列起動時の実務ポイント
- ログは stderr、結果は stdout(競合時の混入を防ぐ)
xargs -Pや&+wait -nで並列化する際、出力先は衝突させない(ファイルは per-job にし、最後に結合)- ロック粒度を定める:プロセス全体/ファイル単位/キー単位(細かすぎてもデッドロック誘発)
- NFS 上では
flockは不安定になりうる。mkdirロックか、アプリ側のDB/キューを使う
“コピペ可”テストブロック(最小)
#!/usr/bin/env bash
set -Eeuo pipefail
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
lock="$TMP/t.lock"
# 1) flock 排他が効く
{
exec {a}>"$lock"; flock -x "$a"; sleep 0.5; echo A
} &
sleep 0.1
{
exec {b}>"$lock"; if flock -n "$b"; then echo "NG: should block"; exit 1; else echo B; fi
}
wait
# 2) mkdir ロック
ld="$TMP/ld"; mkdir "$ld" && echo L1 || { echo "NG mkdir"; exit 1; }
if mkdir "$ld" 2>/dev/null; then echo "NG lock"; exit 1; else echo L2; fi
rm -rf "$ld"
echo "PASS"
失敗しやすい点(アンチパターン)
- ロックを取ったファイルと別のファイルを書き換える
→ 保護対象とロック対象を一致させる(同じディレクトリ階層で意味づけ) trapを忘れて残骸ロック
→EXIT/RETURNで必ず掃除。異常系でも実行されるように- NFS で
flock頼み
→mkdirロックへ切替、あるいはローカルに集約してから同期 - タイムアウトなしで永遠待機
→-nか-wを使い、待機→ログ→終了の方針を固定 - PIDファイルの陳腐化未対策
→kill -0 $pidで生存確認。ダメなら** stale cleanup **
運用メモ(実務)
- ロックファイルの場所は
/run/(揮発)または/var/lock/(権限注意)、一時なら/tmp/ - 命名規則:
appname.resource.lock/appname.<hash(key)>.lock - 監視:ロックの滞留(長時間占有)をメトリクス化(
stat -c %Yで時刻確認) - エラーポリシー:ロック取得失敗は非エラー終了(0)か専用コード(例: 75 “tempfail”)で区別
互換性と移植性
flock(1)は Linux 標準だが macOS では非搭載のことがある(Homebrew の util-linux 等で補完)。POSIX 互換を重視するならmkdirロックへ寄せる- NFS の
flockは信頼できないことがある。NFSv4 でも運用と実装差があるため、共有 FS ではmkdirを
セキュリティと安全設計
- ロックファイル・ディレクトリは パーミッション 600/700(
umask 077)で作成 - ロック名に外部入力を直入れしない(ハッシュ化)
- クリティカルセクションでは一時ファイル名に
mktempを使い、シグナルで掃除(trap 'rm -f …' INT TERM EXIT)
