排他処理

複数のプログラムが1つのファイルに同時に書き込みを行なうと 整合性が取れなくなる。たとえば,あるファイルに書かれている数値を 1だけ増やすプログラムを2つ走らせたとする。最初,ファイルの内容が

10

だったとして,1増やすプログラムを2回動かすと中味は12になる はずである。しかしほぼ同時にプログラムを動かすと 以下のようになる。

時刻プログラムその1の流れ↓ プログラムその2の流れ↓
0ms起動開始
10ms元ファイルを開く起動開始
20ms10を読み取る元ファイルを開く
30ms変数に1を足す10を読み取る
40msファイルに書き込む変数に1を足す
50ms終了ファイルに書き込む
60ms終了

結果は11になる。

このように,あるファイルの中味を読み取りなんらかの修正を加える プログラムは,同じプログラムが同時進行している可能性を考慮し,

のような注意を払う必要がある。

同時に2つのプロセスが同じ処理を行なわないようにすることを 排他処理 という。ファイル修正の排他処理には ファイルロックが用いられる。

Rubyにおけるファイルロック

Rubyでは Fileクラスflockメソッドでロックを行なう。

まず,ファイルロックのないまずい処理例を示す。先に用いた dbm-ops.rb プログラムを 同時に動かす実験をしてみる。

ktermその1

./dbm-ops.rb -a
あいうえお,12345
[C-d]

ktermその2

./dbm-ops.rb -a
かきくけこ,abcde
[C-d]

両プログラムを起動してから, 各ウィンドウでデータ入力を行ないC-d をタイプする。 すると,あとで終わらせた方のデータしか追加されていないことが分かる。

ファイルロック処理を追加してみる。ロックの必要な処理の前に ロック設定処理を,後ろにロック解除処理を入れる。

lockfile="ロックファイルの名前"
open(lockfile, "w") do |lf|
  lf.flock(File::LOCK_EX)    # ロック設定
  DBM.open(datafile) do |x|
      :
  end
  lf.flock(File::LOCK_UN)    # ロック解除
end

dbm-flock.rb

ktermその1
先に起動する

./dbm-flock.rb -a
./database operation start
いろはにほへと,12345
[C-d]

ktermその2
その1を終わらせる前に起動する

./dbm-flock.rb -a
あたらしい朝,いえーい
[C-d]
./database operation start
(その1終了後処理が始まる)

このプログラムではflockを行なうファイルとしてダミーファイルを openしたが,これは排他処理を行なう必要のあるファイル形式がdbmだからで, 普通にopenして修正を行なうファイルの排他処理を行なうなら, そのファイルをopenしたオブジェクトでflockすればよい。

PStoreはtransactionブロックそのものが排他処理を 行なう。

flockの注意

先述のとおりflock処理は極力短くする。さもなくばロック解除待ちの プロセスが溢れかえる。dbm-flock.rb は,新規データの入力処理もロック内で行なっているので,入力に 時間がかかる場合でもずっとロックがかかったままとなる。データベース 更新処理のみをロックするように書き換える。データを追加する部分だけを 抜き出したプログラム dbm-add-flock.rb を示す。

#!/usr/bin/env ruby
require 'dbm'                   # dbmクラスを利用
datafile = "./database"         # データベースファイル名
lockfile = datafile+".lck"      # ロックファイル名

data = Hash.new
while csv=gets                  # 先にCSVデータを読んでハッシュに代入
  if /([^,]*),(.*)/ =~ csv
    data[$1] = $2
  end
end

open(lockfile, "w") do |lf|
  lf.flock(File::LOCK_EX)
  DBM.open(datafile) do |x|
    STDERR.puts "#{datafile} update starts."
    for key, val in data
      x[key] = val
    end
    sleep 2                     # 本当は一瞬で終わるが少し時間を掛ける
    STDERR.puts "#{datafile} update ends."
  end
  lf.flock(File::LOCK_UN)
end

排他処理の必要なファイルを修正する場合は File::LOCK_EX (排他ロック)を指定するが,読み取りのみの場合は File::LOCK_SH(共有ロック) を指定する。

デッドロック

複数のプロセスが互いに別プロセスのロック解除を待ち続けて 永遠に解除が起きない状態を デッドロック という。

たとえば,2つのファイル A と B があり,それぞれに 数値が書かれていたとする。このとき

という2つのプロセスがほぼ同時に走ったとする。プロセス1は 「AをロックしてからBをロック」し。プロセス2は 「BをロックしてからAをロック」しようとしたとする。もし, 「プロセス1がAをだけをロック,プロセス2がBだけをロック」した状態に なったら,この状態は永遠に解除されない。

この例の場合,デッドロックを回避するには複数の資源をロックする順位を 決めておく。AとBであれば,A→Bという順番でのみロックするように 設計する。

一般的には,ロックすべき資源は多数あるのでロックを完ぺきに解除するの は難しい。全体的に可能性を極力下げるよう設計する。

これらを注意するのと同時に,デッドロック回避が本質的に 難しいことを考慮して,

などをしておく。


本日の目次

yuuji@e.koeki-u.ac.jp