ファイルロックの必要性
そふぃのPHP入門 >> PHP実践リファレンス >> ファイル操作 >> ファイルロックの必要性

ファイルロックの必要性

同時アクセスの仕組み

ファイルへの書き込みなどをする場合、ただ単に書き込む処理をしとけば目的の物は出来上がります。カウンターなんかは「+1」した値を書き込み直すだけなんで仕組みとしては単純なもんです。但し!!、ここでは「同時アクセス」というものを考えていきます。Webではそれぞれが違ったPCから1つのサイトに同時にアクセスできてますね、あまり意識してないだけで。これがファイルへの書き込みを同時に行った場合なんかはどうなるでしょう。

カウンターを例に挙げて説明します。たまたま同時にアクセスしちまったユーザAさんとBさんがいます。ちなみにユーザのAさんにもBさんにも同時アクセスしてカウンターを壊そうなどの悪意は毛頭ありませんし、そもそも自分が他の誰かと同時アクセスしてるなんて意識すらしてないでしょう。それなのに!!分からず屋のプログラム君というのは以下のような動きをしてしまうんです。良く見て下さい。

ここではカウント値を「100」として説明します。AさんとBさんがアクセスしたのでカウント値は「102」になって欲しいわけですが、これが同時アクセスだった場合・・・

  1. Aさんがカウンターファイルの値(カウント値)を読み込む
  2. Aさんが読み込んだ値は100
  3. Bさんもカウンターファイルの値を読み込む
  4. Bさんが読み込んだ値も100
  5. (Aさんのカウント値を書き込む前にBさんに値を読み込まれたので2人とも同じ値となる)
  6. Aさんのカウント値+1
  7. Aさんのカウント値が101になる
  8. Bさんのカウント値+1
  9. Bさんも読み込んでる値が100なのでカウント値が101になる
  10. Aさんのカウント値「101」がファイルに保存される
  11. Bさんのカウント値「101」がファイルに保存される
  12. (2人ともカウント値が101になってしまう)

大雑把に言うと、AさんとBさんの2回アクセスがあったにも関わらず、カウンタの値は1しか増えてないわけです。困りましたねぇ。カウント値なんて大体の値でいいやという人はあんま困らないかもしれませんがw、これが掲示板などだったらどうなるか分かるでしょうか。

ほんのちょっと先にアクセスしてたAさんの書き込みはBさんの書き込みに上書きされて吹っ飛びますよね。カウンターならせいぜい値に誤差が出るくらいだからいいや、と思った方もいるでしょうが、これだけじゃなく、カウント値のログ自体が吹っ飛んでしまう現象ってのも簡単に考えつきます。そういった1例はもう少し後で説明してます。

さて、上記のような現象は困るのでどうにかしたい訳ですが、ここでは「ファイルロック」という機能を使います。ちなみに、あまり怖がらせたくはないのですが、ログが吹っ飛ぶ原因というのはこれだけじゃあありません。話がそれてしまうのでここでは詳しく触れませんので、セキュリティ関連のサイトを探してみて下さい。そういうトコには怖くなってプログラムやめようかと思うような事がたくさん書かれてます。私はなるべく初心者さん向けに書いてるつもりなのであまり難しそうな事を要求したくないのですが、セキュリティに関しては必要だと思うので、時間をかけながらでもいいです。分からなければすぐに全てを理解する必要はありませんが、いずれ理解できるように常に頭の片隅において意識しておくようにして下さい。

ファイルのロック

さて、今回使うファイルロックとはどういうものかというと、Aさんがアクセス中のファイルはロックしてしまい、他の人がアクセスできないようにする、という措置をとります。他の人がアクセスできないようにしたらBさんのカウント値はどうするんだ、という素朴な疑問もあるでしょうがw、Bさんには単純に待っててもらうだけです。だからって、何もBさんのブラウザに「しばらくお待ち下さい」なんて無粋な出力をするつもりはありません。待つのはPHP内部での話ですので、Bさんという方にご迷惑をかける訳ではありません。

ファイルロックの仕組みは大体こんな感じです。この場合はAさんのほうがほんの少しだけ(0.01秒とか・・・)早いって設定です。

Aさん Bさん
ファイルを開く  
ファイルをロック ファイルを開こうとする
ファイルを読み込む ロックされているので待つ
カウント値+1 待機
ファイルに書き込む もうちょい待機
ロック開放してさようなら ずっと待ちわびてたの・・
  喜び勇んでファイルを開く
  ファイルをロック
  待った甲斐あって正常に読み込み
  カウント値+1
  ファイルに書き込む
  ロック開放してさようなら

なんとなくうまくいきそうな気がしてきたでしょうか。

ちなみに、今まで出してきたAさんBさん・・・・PHPが「人物」としてのAさんBさんを認識してるわけありません。PHPはこういう処理を「プロセス」として認識します。AさんBさんじゃなくて「Aというプロセス」「Bというプロセス」と読み替えてみて下さい。「プロセス」とはプログラムを管理するための1単位です。AさんからのアクセスはAというプロセス、BさんからのアクセスはBというプロセスとして認識する事によって、別の人からのアクセス(つまり別のプロセス)だと認識している訳です。

話をファイルロックに戻します。ロック方法には他のプロセスからの読み込み・書き込みの両方を禁止する「排他ロック」と「書き込みのみ禁止」、つまり他プロセスからも読み込みだけはできる「共有ロック」ってのがあります。上記のカウンターの例ではこの共有ロックを使っちゃいけません。どうしてか分かるでしょうか。「書き込みもするから」という考えを持った人はまぁ近いですが、問題はそれだけではないのです。

最初の例で、そもそもの問題が「同じ値を読み込んでしまった」事にあるのはお分かりでしょうか。この例の場合はカウントアップの処理やファイルへの書き込み処理に問題はないのです。「同じ値を読み込んでしまったので、分からず屋のプログラム君は同じ値をカウントアップして同じ値を書き込んでる」のです。つまり、プログラム君に同じ値を読み込ませるのがダメなわけです。ゆえにこの場合は読み込みすらしちゃあだめ!!っていう「排他ロック」の方を使います。

恐怖のログ飛び

さて、ロックの仕組みが分かったところで、次にもう少し怖い例を見てもらいます。これを見ながらどういう風にロックをかけたらいいか考えてみて下さいね。

こっちは完全にカウント値が吹っ飛んでしまう仕組みのほんの1例です。ここで「なし」って言ってるのは使う関数によってfalseだったり0だったりしますが、ややこしいのでとりあえず「なし」で統一です。

  1. Aさんがファイルを開く
  2. Aさんが読み込む値は「100」
  3. Aさんのカウント値+1
  4. ここでBさんが颯爽と登場してファイルを開く
  5. 「101」を書き込むつもりのAさんはファイルの中身を消去
  6. Bさんのカウント値はそんな事ともつゆ知らず「なし」と認識される
  7. カウント値には何の疑問も持たれる事なくAさんの101が書き込まれる
  8. Bさんのカウント値を+1したいけど値がないので・・・・・カウントアップ処理は中断
  9. Aさんはファイルを閉じてさようなら
  10. Bさんは値がなかったので素直に値なしを書き込む
  11. Bさんも何の疑問も持たずにファイルを閉じてさようなら

今回は見事にログが吹っ飛んでしまいましたね。ログが吹っ飛ぶと言っても、ファイル自体が壊れる事は滅多にありませんのでご安心を。吹っ飛んでいるのは大事なデータ、つまりファイルの中身です。

こっちでの問題は「ファイルが空と認識される一瞬のスキがある」って事です。5番のところですね。書き込もうとする時に値を消去してますが、こういったスキは作っちゃいけません。PHPの場合、fopen()を「w」モードで開くとファイルの中身が空になります。これは非常に便利なのですが、運が悪ければログが吹っ飛ぶという事を意味しています。

問題自体は分かったわけですが、ここからはさらに突っ込んで上記の流れを見てみます。

まず、fgets()などのファイル内容を読み取る関数ですが、大抵はエラーの時にfalseを返します。エラーというのは何もファイルが存在しない、などの場合だけではありません。ファイルが存在しても、ファイルの中身が空ならfalseが返されます

私はPHPの型の柔軟性から、空ファイル=空文字列「""」=「0」・・のように認識してくれるかもとか思ってたのですが、試してみたところ、思いっきり「false」が返ってきました。例外はfread()関数くらいで、これはファイルの中身が空でもfalseを返さず、あくまで読み込んだ内容を返します。

さて、falseを返された場合、カウント値には「false」が代入されている訳ですがそのままカウントアップすると「false++」です。これはやっぱり「false」のままなのです。つまり、「false」を「数値の0」と認識させたい訳ですが、「false+1」として変数に再代入すれば計算式の関係で数値と認識されますし、どうしても「++」が良ければキャスティングなどで無理やり型変換、ですかね。この辺は「どのようにしたらどの型として認識されるか」の細かい知識が必要になりますので徐々に身に付けてもらうとして、今回の場合は、本来整数型(・・・もし他の型が入ったとしてもせいぜい21億超えた浮動小数点型)しか入らないはずのカウント値というものを型変換しなければならない時点でスクリプトの設計に問題があります。じゃあfread()関数使うか、インクリメント(++)しなきゃいい、というのは根本的な解決になってません。それでもカウント値のリセットは起こります。上記の例のようにログが空にならないというだけで値が「0」にはなってしまいます。

さらに、ファイルに書き込みたいときに使うfwrite()(もしくはfputs())が何かのエラーを出しそうなもんですが、fwrite()(もしくはfputs())は書き込んだバイト数を返してきます。エラーの時はfalseを返してくれますが、上記の例のような場合はエラーではないのでfalseを返してくれません。返してくるのは「int(0)」という値で、つまり・・・・「0バイト書き込んどいたよ」と言って来てる訳です。分からずやですねw。まぁ・・・fwrite()で書き込もうとしてる時点ではすでに値が吹っ飛んでるので今更何かのエラーを出されても困りますw。

間違ったロック方法

さて、たらたらと動作の説明をしてきましたが、上記の事を踏まえた上で、間違ったロック機能を盛り込んだ次の流れを見て下さい。最初に断っておきますがカウンターを作る時は、以下のような流れにはしてはいけません。「w」モードという便利な機能を利用した以下のような流れにすると、ファイルポインタの操作が必要ないのでついついやってしまう人もいるのですが、これだとログが吹っ飛ぶ可能性はある・・・というか全然回避されてません。

  1. ファイルを読み込み専用モード「r」で開く
  2. 共有ロックをかける
  3. カウント値を読み込む
  4. 読み込んだ値を変数に格納
  5. ファイルを閉じる
  6. 今度はファイルを新規書き込みモード「w」で開く
  7. 排他ロックをかけて
  8. 変数化しておいたカウント値を書き込む
  9. ファイルを閉じる

どうして?ロックもしてるじゃん、と思われた方はよく考えて下さいね。

まず、最初のロックは共有ロックです。共有ロックは「読み込みは可能」なんですよ。つまり、わざわざロックをかけているにも関わらず、「2重読み込み」の問題は全然回避されてません。

さらに、「w」モードでログを自ら消去してる点です。変数に格納してちゃんとおいてるじゃん、と思われた方は思い出して下さい。「変数は上書きされる」んです。上書きされなきゃ変数の意味がありません。「w」モードで開き、ファイルの排他ロックをかける寸前に他のプロセスが「カウント値を読み込む」という処理をしている可能性があります。そうなった場合にはカウント値のリセットもしくは恐怖のログ飛びがおこります。え??え??他のプロセスが読み込めるの??ロックは??と思った方はもう一度上の「共有ロック」がどんなロックかってのを読んで下さい。

じゃあ最初のロック、つまり2番の処理を「排他ロック」に変えればばっちりだ!!と思うかもしれませんが、そうしたからといって、あまり解決にはなりません。「ファイルを閉じる」という処理の時にファイルロックは無条件に破棄されます。つまり、最初のロックが排他ロックであってもファイルを閉じた時にすでに待ってるプロセスがあれば無条件に割り込まれるのです。そうなると、「w」モードで開いた時の真っ白(というか値なし)なファイルを読み込まれる可能性もある、って事になりますね。

実際どんくらい飛ぶんだ?と思ったので、テストしてみました。上記のスクリプトで同時アクセスを何回か発生させてみたのですが・・・・見事なくらい何回も飛びました^^;。テストの詳細は長くなるので省きます。

今回は大分難しい話になってしまいましたが、上記の流れをよく理解すると、カウンタースクリプトは以下のようになります。同じようにこっちのスクリプトでもテストしてみましたが、1回も壊れてくれませんでしたので、以下のように組んでおけばまず大丈夫でしょう。

  1. ファイルを上書きモード「r+」で開く(r+は読み書き両方可能です)
  2. 排他ロックをかける
  3. (これでカウント値も残ったまま、誰も触れなくなりました)
  4. 誰も触れなくなったところで、安心してカウント値を読み込む
  5. カウント値を+1
  6. ファイルポインタをちゃんと先頭に戻す
  7. 新しいカウント値を書き込む
  8. (前のカウント値は上書きされるので自然に消滅)
  9. ファイルロックを破棄する
  10. ファイルを閉じる

ちなみに、ファイルを閉じる時にファイルロックも自動的に破棄されると説明しましたが、そのためか「ファイルロックの破棄」という処理はあまり書かれる事はありません。書かなくても自動でやってくれるから問題ない、って事ですね。

こういった排他ロックをかけて誰も触れなくする事を「排他制御」といい、データベースなどにも使われていますが、今回紹介したファイルロックは排他制御の初歩です。・・・・初歩と聞いてへこんだ方もいるかもしれませんが、排他制御はそれだけで1つのコンテンツができるだろ・・・ってくらいまだまだあるのです。ただ、まずはファイルロックの仕組みをきちんと理解して下さい。現在の「ファイル操作」というコンテンツで排他制御を紹介するのはこのくらいが限界なのですが、排他制御についてはネット上にさまざまなドキュメントがありますので興味のある方は検索してみて下さいね。

大分長くなりましたのでファイルロックのサンプルスクリプトは次のページで出します。ファイルロックを実現する関数すら紹介しませんでしたが、ここでは同時アクセスの仕組みとロックが必要な意味を何とかして理解して下さい。