Bashでロックを取る方法

前回に続いてBashネタで、Bashで排他ロックを取る方法について。

並行プログラミングをしようとするならば、排他は必要となる。シェルスクリプトでも並行性について考慮しなければならない場面があるが、アトミックにロックを取る手段はシェルスクリプトだけでは無理で、OSの機能に頼らなければならない。

3つのやり方を見つけたので、メモしておく

  1. lockfileコマンド
  2. flockコマンド
  3. 自前のコマンド

lockfileコマンド

lockfileというコマンドがある。ただし、必ずしもどの環境にあるとは限らない。

LOCKFILE="/tmp/example.lock"
lockfile "${LOCKFILE}"

# 排他したい処理

rm -f "${LOCKFILE}"

上のようにLOCKFILEというファイルをlockfileコマンドでつくって、排他したい処理を行い、最後にLOCKFILEを削除する。

lockfileコマンドはLOCKFILEがあるときは削除されるまで待つ。オプションで待ち時間やリトライの回数等を設定できる。

ファイルの作成、削除はアトミックに行われることを利用している。

flockコマンド

これもどの環境にもあるとは限らないが、flockコマンドを使う方法。Linux限定であれば、これが一番いいと思う。このコマンドはファイルのありなしでなくて、アドバイザリロックという機能を利用します。

LOCKFILE="/tmp/example.lock"

(
  flock -w 10 -x 200 || {
    echo "ERROR: lock timeout" 1>&2
    exit 1;
  }

  # 排他したい処理

) 200> "${LOCKFILE}"

上の例は10秒間ロックを取得できなかったらエラーとなります。サブシェルがLOCKFILEを200番のファイルディスクリプタで開いて、flockコマンドが200番のファイルディスクリプタで開かれているファイルのアドバイザリロックを取得します。サブシェルから抜けるとLOCKFILEは閉じられるので、ロックが開放されます。例の場合は、LOCKFILEは最初だけ作成されますが、削除はされません。既存のファイルを開いてロックを取得することも可能です。

自前のlockコマンド

環境を選びたくないなら、自前でlockコマンドを作ってしまうというのも手段だ。

LOCKFILE="/tmp/example.lock"

function my_lock
{
  local start_time cur_time
  start_time=$(date +%s)
  local timeout=10

  while true; do
    ( umask 0777; : > "${LOCKFILE}" ) 2> /dev/null
    if [[ $? -eq 0 ]]; then
      return 0
    fi
    cur_time=$(date +%s)
    if [[ $((cur_time - start_time)) -gt ${timeout} ]]; then
      echo "ERROR: lock timeout" 1>&2
      return 1
    fi
  done
}

function my_unlock
{
  local exit_code=$?
  rm -f "${LOCKFILE}"
  exit "${exit_code}"
 
}

(
  mylock || exit
  trap  "my_unlock" EXIT HUP INT QUIT TERM

  # 排他したい処理
)

多分、lockfileコマンドとしていることは同じで、ファイルの作成に成功したらロックが取得でき、ファイルを削除したらロックを開放する。trapを使うのは処理が失敗したり、途中で止めたときでも、ロックを開放することを保証するためである。