業務システム開発・保守の実務メモを発信中

バッチの二重起動を防止する方法|Windows CMDとBashでmkdirロックを使う

定期実行しているバッチやシェルスクリプトで、前回の処理が終わる前に次の処理が始まると困ることがあります。

たとえば、同じCSVを二重に取り込む、同じファイルを同時に更新する、途中の一時ファイルを別プロセスが消してしまう。こういう事故は、処理本体が正しくても起きます。

この記事では、Windows CMD と Bash の両方で、mkdir を使って二重起動を防ぐ最小コードを作ります。

一般的にはロックファイルと呼ばれることが多いですが、ここでは通常ファイルではなく、ロック用ディレクトリを作ります。ポイントは、同じ名前のディレクトリ作成に成功できるのは先に実行した1つだけ、という性質を使うことです。

目次

なぜファイルではなくディレクトリを作るのか

ロックと聞くと、次のような処理を思い浮かべるかもしれません。

Markdown
ロックファイルがあるか確認する
なければロックファイルを作る

この形は分かりやすいのですが、確認と作成が別々の処理になります。ほぼ同時に2つのプロセスが起動すると、どちらも「まだロックがない」と判断してしまう余地があります

今回使うのは、mkdir でロック用ディレクトリを作る方法です。

Markdown
mkdirでロック用ディレクトリを作る
作れたら本処理へ進む
作れなければ別プロセスが実行中として終了する

Windows の mkdir はディレクトリを作成するコマンドで、Microsoft Learn では md と同じコマンドとして説明されています。また、Windows API の CreateDirectory は、指定したディレクトリがすでにある場合に失敗します。

Bash側でも、GNU Coreutils の mkdir はディレクトリを作成し、成功時と失敗時で終了ステータスが変わります。-p を付けずに同じ名前のディレクトリを作ろうとすれば、すでに存在する場合は失敗として扱えます。

つまり、Windows CMDでもBashでも、同じ考え方でロック取得を書けます。

Windows CMDの例

まずは Windows CMD のバッチファイルです。

この例では、スクリプトと同じ場所に run.lock というロック用ディレクトリを作ります。作成できた場合だけ本処理へ進み、作成できなかった場合は別プロセスが実行中とみなして終了します。

BAT (Batchfile)
@echo off
chcp 65001 >nul
setlocal

set "SCRIPT_DIR=%~dp0"
set "LOCK_DIR=%SCRIPT_DIR%run.lock"

rem ロック用ディレクトリの作成に成功したプロセスだけが本処理へ進む
mkdir "%LOCK_DIR%" 2>nul
if errorlevel 1 (
  echo LOCKED: another process is already running.
  exit /b 1
)

echo START: main process

rem ここを実際のバッチ処理に置き換える
ping -n 6 127.0.0.1 >nul

echo END: main process

rem 正常終了後にロックを削除し、次回実行できる状態へ戻す
rmdir "%LOCK_DIR%"
exit /b 0

mkdir "%LOCK_DIR%" が、この例のロック取得です。

成功した場合は終了コードが0なので、そのまま本処理に進みます。失敗した場合は errorlevel 1 の分岐に入り、メッセージを出して終了します。

ping -n 6 127.0.0.1 >nul は、処理に時間がかかっている状態を作るためのダミーです。実際に使うときは、この部分をバックアップ、ファイル変換、データ取り込みなどの本処理に置き換えます。

最後の rmdir "%LOCK_DIR%" は、正常終了後に次回実行できるようにするための解除処理です。これを忘れると、次回以降はずっと「実行中」と判定されます。

Bashの例

次は Bash です。

Bashでも考え方は同じです。mkdir に成功したら本処理へ進み、失敗したら別プロセスが実行中として終了します。

Bash
#!/usr/bin/env bash
set -u

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
LOCK_DIR="$SCRIPT_DIR/run.lock"

# ロック用ディレクトリの作成に成功したプロセスだけが本処理へ進む
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
  echo "LOCKED: another process is already running."
  exit 1
fi

# シェル終了時にロックを削除し、正常終了後に再実行できる状態へ戻す
cleanup() {
  rmdir "$LOCK_DIR"
}
trap cleanup EXIT

echo "START: main process"

# ここを実際のシェルスクリプトの処理に置き換える
sleep 5

echo "END: main process"

Bash側では trap cleanup EXIT を使っています。

EXIT に登録した処理は、シェルが終了するときに実行されます。ここでは cleanup 関数で run.lock を削除しています。

CMD版では最後に rmdir を直接書きましたが、Bashでは trap に寄せたほうが、途中で exit する処理を増やしたときにもロック解除をまとめやすくなります。

二重起動したときの動き

実際に、1回目の処理が動いている間に2回目を起動しました。

ここでは分かりやすいように、先に起動したほうを first、後から起動したほうを second として記録しています。first_exitsecond_exit は、スクリプトの外側で取得した終了コードです。

Windows CMD の1回目は、本処理へ進んで正常終了しています。

Markdown
first_exit=0
START: main process
END: main process

同じタイミングで起動した2回目は、ロック取得に失敗して終了しました。

Markdown
second_exit=1
LOCKED: another process is already running.

Bashでも結果は同じです。

Markdown
first_exit=0
START: main process
END: main process
Markdown
second_exit=1
LOCKED: another process is already running.

見るべき点は、START: main process が2回出ていないことです。

2つのプロセスを起動しても、本処理へ進んだのは1つだけです。もう1つはロック用ディレクトリを作れなかったため、処理を始める前に止まっています。

正常終了後は再実行できる

二重起動を防げても、正常終了後に次回実行できなければ運用では使いにくいです。

このサンプルでは、正常終了時にロック用ディレクトリを削除しています。そのため、処理が終わったあとにもう一度起動すると、再び本処理へ進めます。

CMDの再実行結果です。

Markdown
cmd_rerun_exit=0
START: main process
END: main process
cmd_lock_after_rerun=false

Bashの再実行結果です。

Markdown
bash_rerun_exit=0
START: main process
END: main process
bash_lock_after_rerun=false

lock_after_rerun=false なので、終了後に run.lock は残っていません。

ここまで動けば、最低限の流れとしては使えます。二重起動中は2回目を止め、正常終了後は次の実行を受け付ける、という運用で欲しい基本動作になっています。

ロックが残った場合

この方法で一番気をつける点は、ロックが残った場合です。

たとえば、処理の途中でプロセスが強制終了されたり、サーバーが落ちたりすると、ロック用ディレクトリだけが残ることがあります。

その状態で起動すると、スクリプトは本処理へ進みません。

CMD:

Markdown
cmd_stale_lock_exit=1
LOCKED: another process is already running.

Bash:

Markdown
bash_stale_lock_exit=1
LOCKED: another process is already running.
tomo

ロックが残っている以上、スクリプト側から見ると「別プロセスが実行中かもしれない」状態です。勝手にロックを消して進めると、本当に処理中だった場合に二重起動を許してしまいます。

このサンプルをそのまま使う場合は、ロックが残ったときに誰が、どの条件で削除するのかを運用で決めておく必要があります。たとえば、処理が本当に止まっていることをログやプロセス一覧で確認してから、手動で run.lock を削除します。

古いロックの自動削除やPIDによる生存確認も考えられますが、それは「残ったロックを安全に消せるか」を判断するための追加設計です。この記事では、勝手に消さずに止めるところまでを最小構成として扱ってます。

この方法が向いている場面

この mkdir ロックは、外部ツールを増やさず、スクリプト単体で二重起動を軽く防ぎたい場面に向いています。

たとえば、次のような処理です。

  • 同じフォルダ内のファイルを順番に処理する
  • 同じ出力ファイルを更新する
  • 前回実行が残っているとデータが重複する
  • タスクスケジューラやcronの設定だけに頼らず、スクリプト側にも防御を入れたい

一方で、複数サーバーから同じ処理が走る場合や、ネットワークファイルシステム上で厳密なロック保証が必要な場合は、この小さなサンプルだけで判断しないほうがいいです。DBやRedisなどを使う排他制御、Linuxなら flock、ジョブ管理側の多重起動制御も候補になります。

まとめ

バッチやシェルスクリプトの二重起動を防ぐなら、mkdir でロック用ディレクトリを作る方法は扱いやすいです。

作れたプロセスだけが本処理へ進み、作れなかったプロセスは処理前に終了する。この形にすると、通常ファイルの存在確認と作成を分けるより、同時起動時の判断を単純にできます。

最小構成で見るべき点は、次の3つです。

  • ロック取得に成功した1つだけが本処理へ進む
  • 正常終了後にロックを削除する
  • ロックが残った場合は、勝手に進めず止まる

まずはこの形で、スクリプト側に最低限の二重起動防止を入れられます。

ロックが残った場合に自動で消すかどうかは、処理内容に合わせて慎重に決めます。処理がまだ動いているのにロックを消すと、二重起動を防ぐ仕組みが逆に事故の入口になります。

最初は「ロックが残っていたら止める」までにしておき、必要になった段階でログ確認、プロセス確認、PID付きロック、ジョブ管理側の設定などを足していくのが現実的です。

よかったらシェアしてね!
  • URLをコピーしました!
目次