排他制御を実現する(シェルスクリプト)

課題

同時に複数のプロセスが処理をしてはならないクリティカルセクションを用意したい。 シェルスクリプトではどのように排他制御を実現することができるのか。

解決法

mkdir コマンドを利用する。

排他制御と mkdir コマンドには一見して関連がないように思えるかもしれない。 排他制御として利用できるのは mkdir コマンドの「すでに同名のディレクトリ(ファイル)が存在しているとき失敗する」性質である。


最初に、比較として、排他制御を行わないために意図通りに動作しない例を示す。

以下のスクリプトでは、2つのバックグラウンドプロセスを作成し、それらが同一のファイルに対して書き込みを行う。 sleep コマンドは本記事用に「意図通りに動作しない」確率を上げるために挿入した。

touch target.txt

(
  echo "start from p1" >> target.txt
  sleep 0.01
  echo "end   from p1" >> target.txt
) &

(
  echo "start from p2" >> target.txt
  sleep 0.01
  echo "end   from p2" >> target.txt
) &

wait

本来意図した動作結果は以下のどちらかだ。 つまり、片方のプロセスがファイルへの出力を完全に終えたあとに、もう片方のプロセスがファイルへの出力を行うということを意図していた。

$ cat target.txt 
start from p1
end   from p1
start from p2
end   from p2
$ cat target.txt 
start from p2
end   from p2
start from p1
end   from p1

しかし、最初に示したスクリプトを実行すると、以下のようになってしまう。 このようになる原因は、もちろん、排他制御を行っていないからだ。 片方のプロセスが「start ~」の書き込みを行って sleep している間に、もう片方のプロセスが書き込みを始めてしまっている。

$ cat target.txt 
start from p1
start from p2
end   from p1
end   from p2


上記の状況に対して、今度は排他制御を行って、意図通りの動作を実現しよう。

さっそくだが排他制御を実現するスクリプトを以下に示す。 キーとなるのは冒頭で説明した mkdir コマンドの存在だ。

ちなみにこのスクリプトでは動作状況を把握できるようにいくつか echo コマンドを挿入している。 また、今回は mkdir コマンドの引数になっている lockdir 変数の値については特に触れない。 ごちゃごちゃしているが、既存のディレクトリ名と被らないようにするための記述である。

lockdir="${TMPDIR:-/tmp}/${0##*/}.$(date +%Y%m%d_%H%M%S).$$"

touch target.txt

(
  while true
  do
    if mkdir "$lockdir" 1>/dev/null 2>&1; then
      break;
    else
      echo "p1: lock failed"
    fi
  done

  echo "p1: lock succeed"

  echo "start from p1" >> target.txt
  sleep 0.01
  echo "end   from p1" >> target.txt

  rmdir "$lockdir"
) &

(
  while true
  do
    if mkdir "$lockdir" 1>/dev/null 2>&1; then
      break;
    else
      echo "p2: lock failed"
    fi
  done

  echo "p2: lock succeed"

  echo "start from p2" >> target.txt
  sleep 0.01
  echo "end   from p2" >> target.txt

  rmdir "$lockdir"
) &

wait

それぞれのプロセスの先頭部分に「ロックを獲得する」ことを表すコードを配置している。 それはつまり mkdir コマンドを内包した while ループだ。 lockdir を作成することに成功したら、while ループを抜けてファイル出力のコードに進むことができる。 逆に、lockdir を作成することに失敗すると、while ループを抜けることができず、何度も lockdir の作成を試みることになる。 何度も試みるという意味では、この排他制御はスピンロック式であるということに注意したい。

先に「ロックを獲得した」プロセスはファイル出力を行い、最後に、先頭部分で作成した lockdir の削除を行う。 これはつまり「ロックを解放する」ことに対応する。 その後、もう片方のプロセスが「ロックを獲得する」ことに成功する。 そして先のプロセスと同様の処理をたどる。

排他制御を実現するこのスクリプトを実行した結果を以下に示す。 スクリプト実行時の標準出力は、それぞれのプロセスが「ロックを獲得する」までの過程を表している。 とくに後続のプロセス(p2)が 11 回も「ロックの獲得」を試みたことがわかるだろう。 そしてファイルの内容を見ると、意図通りの結果となっていた。

$ ./mutex.sh 
p1: lock succeed
p2: lock failed
p2: lock failed
p2: lock failed
p2: lock failed
p2: lock failed
p2: lock failed
p2: lock failed
p2: lock failed
p2: lock failed
p2: lock failed
p2: lock failed
p2: lock succeed
$ cat target.txt 
start from p1
end   from p1
start from p2
end   from p2

所感

排他制御は大変なので、なるべくなら使わない方法を探しましょう