Perlによるファイルの排他制御を考える flock編
Perlでファイルの排他制御を行うにあたり、実運用に耐えるコーディングについて考えます。
今回はflockを用いた排他制御です。
flockは完璧ではない
flockによるファイルの排他制御は「ファイルが壊れないようにする」処理ではありません。
「ファイルを壊れにくくする」処理です。
本気で壊れたらまずいファイルについては、排他制御を必要としない運用方式や、データベース等の導入を検討すべきです。
マルチスプロセス環境でのflockについて
PHPではマルチプロセス環境でのflockに信頼性がありませんが、Perlの場合は気にしなくても良いようです。
ただ、flockがスレッドレベルロック+プロセスレベルロックなのかどうは分かりません。
懸念する点
open関数
open関数は正しく使わないと問題が生じます。
以下の一文は、openを行った際にファイルサイズをゼロ(ファイルを空っぽ)にしてしまいます。
open(out,"> test.txt");
読み書きモードとして、openした時点でファイル内容は維持するためには、以下の一文を使います。
open(out, "+< D:/test.txt");
なお、追記も完全ではなく、同時アクセスが生じた場合はデータが混ざる場合があります。
flock関数
flock関数は、その挙動を理解して使わないと運用に異常を来たします。
flockでは、そのオペレーションに以下の方法を指定する事が可能です。
1 | 読み込みロック+ブロック |
2 | 書き込みロック+ブロック |
5 | 読み込みロック |
6 | 書き込みロック |
8 | ロックを解除 |
1(読み込みロック+ブロック)と2(書き込みロック+ブロック)は、flockしたファイルをブロックします。
つまり、別のプロセスがブロックされたファイルにアクセスすると、ロックを解除するまで待たされる事になるのです。
検討すべき点
ファイルへの書き込みと読み込み
ファイルロックを非ブロックでかけます。
flock関数において、5(読み込みロック)と6(書き込みロック)は、自分がロックをかける事ができた場合は「真」、それ以外は「偽」を返すので、これを利用してループさせます。
ループの際には、サーバに負担をかけないために、sleepさせるのが良いかも知れません。
タイムアウト
alarm関数とeval関数を組み合わせて行います。
プロセスがブロックされていない事が前提ですが、アラームシグナルを受け取りdieする等するのが簡単です。
alarm関数によるアラームの初期化は、evalの内側と外側に記述すると、より安全だと思います。
サンプル
上記の事を加味し、実際にコーディングしてみました。
ファイル書き込み
#!D:/usr/local/Perl/bin/Perl.exe print "◆ファイルを排他制御します。\n"; eval{ # Alarmシグナルキャッチ時 local $SIG{ALRM} = sub { die "timeout"; }; # 前のプロセスの終了〜ファイル書き込みに、10秒間だけ待つ # 一連の処理が10秒間以内に終わらないとタイムアウトします。 alarm(10); # ファイルを読み書きモードで開く # test.txtがないとエラーになります。 if(! open(out, "+< test.txt")){ die "fileopen"; } print "ファイル開きました(10秒間でタイムアウト)。\n"; # ファイルロック # ロックが解除されるまでループ while(! flock(out,6)){ sleep(1); } print "ファイルをロックしました。\n"; # もしテストするとしたら、ここで無限ループ # 以下は無限ループのサンプルです。 #while(){;} # ファイルサイズをゼロにする if(! truncate(out,0)){ die "filetruncate"; } # ファイルの先頭を指定 if(! seek(out,0,0)){ die "fileseek"; } # ファイル書き込み if(! print out "ファイルへ書き込みます。\n"){ die "fileprint"; } print "ファイルに書き込んでいます。\n"; # ファイルをクローズ # ファイルをクローズすると自動的にロックも解除されます。 close(out); print "ファイルをクローズしました。\n"; # Alarmを初期化 alarm(0); }; # Alarmを初期化(dieした時のため) alarm(0); # ファイルをクローズ(dieした時のため) close(out); if($@ =~ /timeout/){ print "ファイル書き込みがタイムアウトしました。\n"; }elsif($@ =~ /fileopen/){ print "ファイルを開けませんでした。\n"; }elsif($@ =~ /fileseek/){ print "ファイルの先頭を指定できませんでした。\n"; }elsif($@ =~ /fileprint/){ print "ファイルの書き込みに失敗しました。\n"; }elsif($@ =~ /filetruncate/){ print "ファイルサイズの変更に失敗しました。\n"; }elsif($@){ print "その他のエラーです。[".$@."]\n"; } if($@){ exit(1); } print "おしまい\n"; exit(0);
ファイル読み込み
#!D:/usr/local/Perl/bin/Perl.exe print "◆ファイルを排他制御して読み込みます。\n"; eval{ # Alarmシグナルキャッチ時 local $SIG{ALRM} = sub { die "timeout"; }; # 前のプロセスの終了〜ファイル書き込みに、10秒間だけ待つ # 一連の処理が10秒間以内に終わらないとタイムアウトします。 alarm(10); # ファイルを読み書きモードで開く # test.txtがないとエラーになります。 if(! open(in, "test.txt")){ die "fileopen"; } print "ファイル開きました(10秒間でタイムアウト)。\n"; # ファイルロック # ロックが解除されるまでループ while(! flock(in,5)){ sleep(1); } print "ファイルをロックしました。\n"; # もしテストするとしたら、ここで無限ループ # 以下は無限ループのサンプルです。 #while(){;} # ファイルを読み込む @in = <in>; # ファイルをクローズ # ファイルをクローズすると自動的にロックも解除されます。 close(in); print "ファイルをクローズしました。\n"; # Alarmを初期化 alarm(0); }; # Alarmを初期化(dieした時のため) alarm(0); # ファイルをクローズ(dieした時のため) close(in); if($@ =~ /timeout/){ print "ファイル書き込みがタイムアウトしました。\n"; }elsif($@ =~ /fileopen/){ print "ファイルを開けませんでした。\n"; }elsif($@){ print "その他のエラーです。[".$@."]\n"; } if($@){ exit(1); } # ファイル内容を表示 print "ファイルの内容 : "; print @in; print "おしまい\n"; exit(0);