2011/01/14

Rubyでのflockの使い方

マニュアルでflockの使い方をみると、File::RDWR|File::CREATを指定していて、"w"を使うと切り詰め(truncate)るからダメだという記述があります。

ruby-1.9.2-p0/file.cに記載されているflockのサンプル

 *     # update a counter using write lock
 *     # don't use "w" because it truncates the file before lock.
 *     File.open("counter", File::RDWR|File::CREAT, 0644) {|f|
 *       f.flock(File::LOCK_EX)
 *       value = f.read.to_i + 1
 *       f.rewind
 *       f.write("#{value}\n")
 *       f.flush
 *       f.truncate(f.pos)
 *     }

たしかに"w"はだめだろうけど"r+"か"w+"ならいけるんじゃなかろうかと思って調べてみました。

ソースコードに記述された各モード文字列の意味

手元にあったruby-1.9.2-p0のio.cをみると、次のような記述があります。

ruby-1.9.2-p0/io.cからの抜粋 (9755-9773行)

 *    Mode |  Meaning
 *    -----+--------------------------------------------------------
 *    "r"  |  Read-only, starts at beginning of file  (default mode).
 *    -----+--------------------------------------------------------
 *    "r+" |  Read-write, starts at beginning of file.
 *    -----+--------------------------------------------------------
 *    "w"  |  Write-only, truncates existing file
 *         |  to zero length or creates a new file for writing.
 *    -----+--------------------------------------------------------
 *    "w+" |  Read-write, truncates existing file to zero length
 *         |  or creates a new file for reading and writing.
 *    -----+--------------------------------------------------------
 *    "a"  |  Write-only, starts at end of file if file exists,
 *         |  otherwise creates a new file for writing.
 *    -----+--------------------------------------------------------
 *    "a+" |  Read-write, starts at end of file if file exists,
 *         |  otherwise creates a new file for reading and
 *         |  writing.
 *    -----+--------------------------------------------------------

"w"や"w+"を使っていると、もしロックに失敗してもファイルの内容が失なわれてしまいます。 もっともLOCK_NBを加えていないサンプルのコードの場合には、ずっと待つので関係ない気がします。

そこでサンプルを考えてみました。2つのスクリプトがtest00.txtというファイルに自分のファイル名を書き込もうとします。

最初に実行するtest00a.rb

#!/usr/local/bin/ruby
#-*- coding: utf-8 -*-
@basedir = File::dirname($0)
file = File::join([@basedir,"test00.txt"])
File.open(file, "w", 0644) {|f|
  f.flock(File::LOCK_EX)
  f.rewind
  f.write($0)
  f.flush
sleep 10
  f.truncate(f.pos)
}

次に実行するtest00b.rb

#!/usr/local/bin/ruby
#-*- coding: utf-8 -*-
@basedir = File::dirname($0)
file = File::join([@basedir,"test00.txt"])
File.open(file, "w", 0644) {|f|
  if f.flock(File::LOCK_EX)
    f.rewind
    f.write($0)
    f.flush
    f.truncate(f.pos)
  end
}

別端末でcat test00.txtをループしながら、スクリプトを実行すると2つめの"test00b.rb"を実行した時点で処理が一時停止したかのようにみえます。

$ while true ; do cat test00.txt ; sleep 1 ;done

...
./test00a.rb
./test00a.rb
./test00a.rb  ## ← test00b.rbの実行直後から、内容のないファイルをcatするため画面には何も表示されない
./test00b.rb  ## ← seep 10の処理が終り、ファイルが上書きされ、その内容が出力される
./test00b.rb
./test00b.rb
...

とはいえ、確実にflockで待機していたtest00b.rbが内容を上書きしているので、意図したような動き自体にはなっているはずです。

ここでLOCK_NBを一緒に使う場合を考えると、実際にはファイルを上書きしなくてもファイルサイズが零になるため問題になるでしょう。

スクリプトを少し変更して、File::LOCK_NBを一緒に使うようなサンプルを作成してみます。

File::LOCK_NBを使った排他制御の例

前節と同様にtest01a.rb, test01b.rbを準備して、それぞれからtest01.txtを自身のファイル名で上書きする事を考えます。

ただし今回はファイルオープンに"r+"オプションを指定します。

最初に実行するtest01a.rb

#!/usr/local/bin/ruby
#-*- coding: utf-8 -*-
@basedir = File::dirname($0)
file = File::join([@basedir,"test01.txt"])
File.open(file, "r+", 0644) {|f|
  f.flock(File::LOCK_EX)
  f.rewind
  f.write($0)
  f.flush
sleep 30
  f.truncate(f.pos)
}

次に実行するtest01b.rb

#!/usr/local/bin/ruby
#-*- coding: utf-8 -*-
@basedir = File::dirname($0)
file = File::join([@basedir,"test01.txt"])
File.open(file, "r+", 0644) {|f|
  if f.flock(File::LOCK_EX|File::LOCK_NB)
    f.rewind
    f.write($0)
    f.flush
    f.truncate(f.pos)
  end
}

これで実行するとファイルの新規作成はできませんが、既存ファイルを準備しておけば期待通りに動きます。

さいごに

表に戻ると、C言語の最初にfopenを習った時は"r"やら"r+"やらのアクセスモードの違いがよく分かりませんでした。

でも確認すればいいんですよね。ただ、経験がない分、それをどういう場面で使えばいいかの想像力が少し十分ではなかったかな。

教えるっていう行為は経験値が足りない人にどう伝えればいいかの部分が難しいんですよね、きっと。

0 件のコメント: