私のパソコン雑記帖

ファイルロック関数の使い方

カテゴリー: PHP
25Aug2006→2Apr2010更新

ファイルロックは本当に必要?

そもそも何故ファイルをロックする必要があるのだろう。それは同時アクセス(書き込み)による衝突があった時しばしばデータが消失することがあるため、と解説されています。しかし数十(数百?)ギガヘルツのクロックで動いているコンピューターの世界で同時アクセスはありうるのだろうか。理屈はともかく現実に起こる確率は無視できるほど小さいのではないだろうか。はじめはそのような疑問を拭えませんでした。

そんな疑問を、CGIやDBのロックと同時実行制御が解消してくれました。そこには、厳密な同時アクセスでなくてもOSのマルチタスク処理の過程で同時アクセスとして扱われてしまう仕組みが解説されてます。これを読んでようやくファイルロックの必要性を得心。


アンロックの問題

更に上記解説では mkdir ロック方式には重大な落とし穴があるといっています。「ロックできるかどうかのテストと実際にロックする操作を同時に行なうことはできる。なのに、ロックを解除するかどうかの判断と実際にロックを解除する操作は同時に行なえない。そのわずかの間隙に他のプロセスが介入して不具合を起こす可能性がある。」 この事は、PHPマニュアル flock の書き込みでも指摘されています。

結論として flock の使用をすすめています。ただし flock 関数はどんなサーバー環境でも使えるとは限りません。汎用スクリプトを制作する人はそれを考慮しなければならないでしょう。私のサーバー環境(xrea)では flock が使えるし、汎用スクリプトを制作する意図も無い。従って flock 関数を用いることにしました。


flock 関数の使い方

ネット上随所に flock を使ったサンプルコードが紹介されています。ただし、自信を持って紹介している人、flock でもうまくいかないと嘆く人、明暗がわかれます。両者の典型例を比較してみます。左が自信派、右が嘆き派。

<?php
$file = "count.txt";
$fp = @fopen( $file, "r+" );
@flock($fp,LOCK_EX);
$count = fgets( $fp, 10 );
$count++;
rewind( $fp );
fputs( $fp, $count );
fclose( $fp ); //ロックは自動的に解除
echo $count;
?>

<?php
$fp=fopen("/tmp/lock.txt", "w+");
if (flock($fp, LOCK_EX)) {
fwrite($fp, "Write something here ");
flock($fp, LOCK_UN);
} else {
echo "ファイルをロックできません!";
}
?>


flock は fopen で得られたファイルポインターにかけます。左の例では fopen モードが "r+" ですが、右の例では "w+" です。これではロックがかかる前にファイルサイズがゼロになる瞬間が生じます。一方左の例では fwrite が使えないので rewind fputs で書き込んでいます。

皮肉なことに右の典型例は"PHPマニュアル flock"に掲示されているサンプルコードなのです。もっとも注意書きがあって、それを日本語版と英語版から引用して併記すると、

注意: flock()は、ファイルポインタを必要とするため、 (fopen()へ引数"w"または"w+"を指定して)書き込みモードでオープンすることにより丸めるファイルにアクセス保護する特別なロックファイルを使用する必要があるかもしれません。
Note: Because flock() requires a file pointer, you may have to use a special lock file to protect access to a file that you intend to truncate by opening it in write mode (with a "w" or "w+" argument to fopen()).

とあります。よく読むと、「この使用例は危険である」と云っているに等しいのではないでしょうか。「special lock file を使う必要があるでしょう」といっていますが具体例がないのでピンとこない。英文の方はともかく、日本語のほうは truncate を"丸める"と訳したため殆ど意味不明になっています(この場合"ファイルサイズをゼロにする"の意)。おそらく注意書き不消化のまま、このサンプルコードをそのまま引用するケースが起こるのではないでしょうか。以下に拙訳を挙げます。

「flock()は、ファイルポインタを必要とするため、(fopen()へ引数"w"または"w+"を指定して)書き込みモードでオープンする時ファイルサイズがゼロになります。この為ファイルにアクセス保護する特別なロックファイルを使用する必要があるでしょう。」

上の2例はいずれもジョブ対象ファイルに直接ロックをかけています。これに対しロック専用ファイルを使う例がありました。そふぃのPHP入門。この方法はジョブ対象ファイル(あるいはコード)をロックファイルと切り離して扱うことができるので便利です。


ロック専用ファイルを用いた実験

テストコードを2つ用意。proxy.txt がロック専用ファイル。プログラムAは sleep(20) を入れて終了までの時間を延ばしています。その他は両者同じ。ブラウザを6画面開き、1画面でプログラムAをスタートし(ブラウザA)、20秒以内に他の5画面でプログラムBを順次スタート(ブラウザB1~B5の順)。microtime 関数は1億分の1秒の桁まで時刻を計測します。

プログラムA

<?php
$fp = fopen("dummy/proxy.txt","w");
print "time1a=".microtime()."<br> ";
flock($fp,LOCK_EX);
print "time2a=".microtime()."<br> ";
print "test"."<br> ";//ジョブ処理
sleep(20); //20秒時間を稼ぐ
print "time3a=".microtime()."<br> ";
fclose( $fp );
print "time4a=".microtime()."<br> ";
?>

プログラムB

<?php
$fp = fopen("dummy/proxy.txt","w");
print "time1b=".microtime()."<br> ";
flock($fp,LOCK_EX);
print "time2b=".microtime()."<br> ";
print "test"."<br> ";//ジョブ処理
print "time3b=".microtime()."<br> ";
fclose( $fp );
print "time4b=".microtime()."<br> ";
?>


実験の結果、6画面の時刻表示がどうなったか時系列で示します。時刻(秒)の上6桁は省略。

ブラウザA
    ロック開始:time2a=6007.54408100
    ジョブ処理
    ロック解除:time4a=6027.55394300 (+20.00986200)

ブラウザB2
    ロック開始:time2b=6027.55394800 (+0.00000500)
    ジョブ処理
    ロック解除:time4b=6027.55398300 (+0.00003500)

ブラウザB4
    ロック開始:time2b=6027.55434200 (+0.00035900)
    ジョブ処理
    ロック解除:time4b=6027.55436000 (+0.00001800)

ブラウザB3
    ロック開始:time2b=6027.55463700 (+0.00027700)
    ジョブ処理
    ロック解除:time4b=6027.55467400 (+0.00003700)

ブラウザB1
    ロック開始:time2b=6027.55511300 (+0.00043900)
    ジョブ処理
    ロック解除:time4b=6027.55516400 (+0.00005100)

ブラウザB5
    ロック開始:time2b=6027.55583700 (+0.00067300)
    ジョブ処理
    ロック解除:time4b=6027.55588100 (+0.00004400)

プログラムBがブラウザB1~B5の上で始動した時、既にプログラムA(ブラウザA上)のファイルロックがかかっているので待機を強いられます。ようやくプログラムAのファイルロックが解除された時、ブラウザB1~B5上で待機していた5つのプログラムBは一斉に自分のファイルロックをかけようと競合状態になります。しかし B2->B4->B3->B1->B5 の順番にそれぞれ ロック->ジョブ処理->アンロック が競合しないで進行しています。見事に捌かれた様子がわかります。

テストコードでは print だけの単純なジョブ処理ですが、通常はここにファイル書き込みなどのジョブコードが入るでしょう(下記例)。このジョブコード自体はファイルロックを気にする必要がありません。要するにジョブ処理の対象ファイルにロックをかけるのではなく、それとは無関係の proxy.txt のファイルポインターにロックをかけているのがミソです。

$flock=fopen("[path]/proxy.txt","w");
flock($flock,LOCK_EX);
$fp=fopen($file,"w");//実際に書き込むファイルを開く
fwrite($fp,$content);//$contentは書き込む内容
fclose($fp);
fclose( $flock);


ユニーク数を取得する

プログラムを組んでいると、繰り返しのないユニーク数を取得したい時があります。rand 関数を使うのが一般的と思いますが、time 関数とファイルロックを組み合わせた簡便法も結構重宝します。以下のスクリプトで、$a が期待される数値です。

<?php //time関数で取得したunix秒数(10桁)を乱数代わりに用いる。
//取得後1秒の遅延時間を設け、その間にファイルロックをかけることで、ユニーク数となる。
$flock=fopen("./library/proxy.txt","w");
flock($flock,LOCK_EX);
$a=time();
$a=trim($a);
sleep(1);
fclose($flock);
//これ即ち・・・過ぎ去った「時」は二度と帰らない・・・
?>



コメント