• ベストアンサー

ロック処理について

ロック処理について my$id="abc"; open(IN,"file"); while(<IN>){ my ($cid) = split(/\,/); if($cid eq $id){&error;} } close(IN); open(OUT,">>$file"); print "$id,$pass\n"; close(OUT); いままで上記のような書き方でも普通にロック機構を使っていたのですが、 ふと、追加書込みなら必要ないのではと思い至りました。 上記のようにファイルに追加書込みする場合は、ロックは必要ないのでしょうか?

質問者が選んだベストアンサー

  • ベストアンサー
  • N60-BASIC
  • ベストアンサー率80% (17/21)
回答No.3

スクリプトの内容から察するに、質問者さんがやりたいことは 「ユニークな(=既存と重複しない)IDを確実に発行したい」 と理解しました。 回答から先に申し上げると、 「CGI実行環境などの、同一スクリプトが複数プロセスで同時に並行実行される可能性があるのであれば、このケースでは絶対にロックが必要」です。 加えて、このスクリプトの場合、ロックの手段や対象を間違えると正常に動作しません。 まず、ご質問にあった「ロックが必要な理由」から。 他の回答者さんの具体例の通り、複数プロセス間において「書き込みタイミング」が重なった場合に、ロックがないと追記のみであってもデータ破壊が発生します。 すでに挙がっている例とは別のパターンとして、AとBの2つのプロセスが以下のタイミングで並行実行されたとします。 ・Aが追記モードでファイルオープン(ファイルポインタはファイル末尾にセットされる) ・Bが追記モードでファイルオープン(ファイルポインタはファイル末尾=Aと同じ位置!) ・Aがファイルに書き込み(ファイルポインタは書き込み末端位置に移動するが、Bのファイルポインタの位置は変わらない) ・Bがファイルに書き込み(Aが書いたデータを上書きしてしまう!もしAの書き込みデータのほうがBより長かった場合には、結果として「Bのデータ+Aのゴミ」が残る) 「ファイルオープン→ポインタを末尾に移動→書き込み→ファイルクローズ」を一度に行うために、ロックが不可欠なのです。 ロックの必要性についてはご理解頂けたかと思います。 次にロックの方法について。 このスクリプトでは「ファイル読み込み」と「ファイル書き込み」がそれぞれ独立したファイルオープン&クローズで行われています。 この場合、「読み」「書き」それぞれ個別にファイルロックをかけても、意図した動作にはなりません。 「読み」と「書き」の間で、データ内容の一貫性が保たれない場合があるためです。 以下に例を挙げます。 - AがID='abc'の重複をチェックするためにファイルを読み、重複がないことを確認 - BがID='abc'の重複をチェックするためにファイルを読み、重複がないことを確認(この時点でAがまだ書き込んでいないため、Bは重複がわからない!) - AがID='abc'に関する情報を追記書き込み - BがID='abc'に関する情報を追記書き込み(結果、'abc'のレコードが2つに!) つまり、「読み」~「書き」を行う間、他のプロセスに割り込まれないようなファイルロックをする必要があるわけです。これを解決するには、2つの方法があります。 ・データファイルをロック対象にする場合は、「読み」「書き」をひとつのファイルオープンで済ませる ・データファイル以外に「ロック専用ファイル」を用意して、「読み」の前にロック、「書き」の後にアンロックすることで他プロセスの割り込みを防ぐ 例として、前者の方法を取った場合のスクリプト例を以下に示します。 my $id="abc"; use Fcntl; sysopen(my $INOUT, "file", O_CREAT|O_RDWR) or &error; # 読み書きオープン、ファイルがない場合は新規作成 flock($INOUT, 2); # 書き込みlock seek($INOUT, 0, 0); # ファイル先頭に移動 # ID重複チェック while(<$INOUT>){ my ($cid) = split(/\,/); if($cid eq $id){&error;} } seek($INOUT, 0, 2); # ファイル末尾に移動(この例ではすでに末尾にいるので省略可)print $INOUT "$id,$pass"; close($INOUT); # unlockはclose()時に自動で行われる 以上です。以下、補足です。 - open()の代わりにsysopen()を使うことで、読み書きモードによるオープンであってもファイルの新規作成が可能です。 - 今回の例ではファイル先頭から末尾まで読み込むので、seek()は省いても動作すると思います。読まずに追記書き込みのみをする場合はseek(FILEHANDLE, 0, 2)は省略しないでください。ファイルオープンしてからロックするまでの間に、ファイルサイズが変わっていることがあります。 - ファイルハンドルをmyで変数化する癖をつけておくと、将来プログラムが大規模になったときも安心です。INやOUTなどのPerl4風ファイルハンドラは名前空間がグローバルになるので、ファイルオープン中の処理が長くなった場合に予期せぬ動作を招くことがあります。

yuutoOK
質問者

お礼

有難うございます。 私にも理解できるように説明してくださり感謝です。 >>・データファイル以外に「ロック専用ファイル」を用意して、 >>「読み」の前にロック、「書き」の後にアンロックすることで他プロセスの割り込みを防ぐ 普段はこの形式でロックするようにしています。他の人のロックの仕方を真似しただけですが…。 ただ、今までは知識もなかったので、なんとなく使っていただけだったのですが、 今回でいろいろと勉強になりました。 みなさまどうも有難うございました。

その他の回答 (2)

回答No.2

いってることとやっていることの意味がよくわかりませんが。 perl の open 関数やprint系の関数は C言語で言うところのストリーム関数(3S)を用いて出力されているようです。 # fopen, fread, fwrite とか ということで、 ファイルへ出力する段階では確かにロックされますが、 ストリーム関数の場合、ライブラリ内部にバッファを持っていて それがいっぱいになるまではファイルへは出力されません。 なので、あんまり大量のデータを出力するとバッファの出力タイミングがずれるために 出力結果が混ざることになります。 これを本当に回避したいなら、低レベル関数やPOSIX::setvbufを調べてください。 でもまあ、一番よいのは一つのファイルを複数書き込みオープンしないことです。 正確に実施したい場合は、 本物のファイルロック関数を使うかロックファイルを作成して調整してください。

yuutoOK
質問者

お礼

ありがとうございます。 perlの知識がほとんどないので、初歩的で見当違いな質問をしていたみたいです。 出力結果が混ざることもあるということで、勉強になりました。 書込みを行う時は必ずロックするように致します。

回答No.1

一番起こりやすいのが、1行がまざることです。 あるプロセスが"abc.."と追記している間に、別のプロセスがファイルをopenして"ABC"と追記してすぐcloseすると、 abcdABCedfgh... となります。 ちょっとテストコードを書いてみましたが、環境が違うと上手く動作しないかもしれません。私の環境だと ...aaaaa !!! ABC !!! aaaa... となりました。 use strict; use warnings; use Time::HiRes qw(usleep); my $file = 'test.txt'; open my $fh, '>', $file or die "$!:$file"; print {$fh} '---', $/; close $fh or die "$!:$file"; my $pid = fork; if ( !defined $pid ) { die "fork"; } if ( $pid ) { # Parent usleep(1000); open my $fh, '>>', $file or die "$!:$file"; print {$fh} ' !!! ABC !!! ', $/; close $fh or die "$!:$file"; wait; } else { # Child open my $fh, '>>', $file or die "$!:$file"; print {$fh} q{a} x 10000, $/; sleep 1; close $fh or die "$!:$file"; exit; }

yuutoOK
質問者

お礼

有難うございます。 perlの知識はファイルの読み書きコードを何とか覚えた程度なので、ロックといえば、カウンターなどで同時アクセスが起こった場合ファイルが空白になってしまう現象を回避するとしか思っていませんでした。 なので、追加書込みならデーターが消えることがない?ので、ロックの必要はないと質問しました。 データが混ざることもあるんですね。勉強になりました。