CGIやDBのロックと同時実行制御

はじめに

検索しても、「ファイルロック・ロック・排他制御の重要性・安全性」について、自分自身が満足できるほどに充実した内容の文書が見付からなかったので、作りました。

この記事は、相当な長文です。長いです。くどいです。しつこいです。悪い例を何種類も挙げ、なぜ失敗するのかを示し、たった1種類の「良い例」を提示するまでに、ものすごい寄り道をします。

排他処理関係では、落とし穴が無数にあります。中途半端な知識でかかると、ほぼ確実にどれかにハマります。だからこそ多くの「間違いの例」を挙げました。その結果長文になりました 。「正しい例」(10行かからずに書けますしこの文章の後半に確かに置いてあります)だけ見せても、その裏の意味を知らない限り、次回また、どれかの罠にハマるでしょうから。

その代わり、この文章を最後まで読み切ったときには、「排他制御」が如何なるものかについて、原理をよく理解してくれるだろう、自然に自分で考えて応用してくれるようになるだろう、と、そう信じています。

この文章の対象

記事の構成

この記事では、CGIでカウンタ作るときとか、スレッドプログラムを作るときとか、データベースで重要な作業するときに必ず必要になる「ロック」「排他処理」の概念について横断的に述べていきます。

ただし、最初に、一番簡単な例だと思われるCGIアクセスカウンタで、問題の根元についてほとんど論じきります。DBのお話は最後に補足的に扱います 。スレッドはほとんど触れません。

作者

naruto/CANO-Lab
mikiso@gf6.so-net.ne.jp

2005年に大学を卒業してコンピュータとは無関係な職業に就いた、趣味プログラマ。PerlとDelphiとMySQLが好き。最近はPHPも好き。

この文書の誤植レポートや改善案を歓迎します。このページへのリンクはご自由にどうぞ。

キーワード

CGI, 排他処理, ロック, 理由, ファイルロック, ロックファイル, Perl, flock, mkdir, symlink, DB, RDB, トランザクション, スレッド, ログ飛び, クリティカルセクション, セマフォ, MySQL, PostgreSQL

記事の難易度

この記事全体の「難易度」ですが、一概に言えません、自作掲示板などのCGIを作っている人にとっては、中上級的な内容に感じるかもしれません。フリーの掲示板CGIとか配布しているサイトでも、ロックが無茶苦茶なところって結構ありますので。

DBを扱おうと思っている人にとって、ここの知識は初級レベルに相当します。 まじめなDBの解説書なら絶対書いてあると思いますので、手持ちの教科書などもっている方は復習のつもりで読むといいかもしれません。スレッド扱おうと思っている人にとっては、ここで述べる知識は入門編もいいところ、知らずにやるのは自殺行為です。

さしあたっての目的

理論上壊れないレベルの「ファイルロック」を実現する。

OSそのものに致命的なバグがあったとか(まぁ考えなくていい)、停電が起きたとか、サーバがインド象に踏まれたといった物理的障害によって壊れることは今は想定しません(バックアップは常に取りましょう♪)。あくまで一般のプログラマが、「 同時アクセスで落ちない」ファイルロックを実現する術を身につけること。

単語の簡単な説明と定義

CGI目的でこのページを見ている人が、全く初見の単語が並んでいて不安に思うかもしれませんので、最初に簡単に説明しておきます。

DB(データベース)

ここでは「データベース」とは、MySQLやPostgreSQL、Oracleなどの「リレーショナルデータベース」、RDBMSを指す単語とします。単純なテキストファイルなどだと、掲示板のデータファイルの大きさが10MBとかになれば死にそうになりますが、まさに「同時に実行する」「ファイルが壊れないようにする」「巨大なデータを正しく扱う」のを目的に開発されたDBシステムでは、100MBでも2GBでも、巨大なデータを高速安全に扱うことができます。単純なCGIに十分慣れた人が「過去ログを別ファイルにしないで3000件溜めてもいいのね?」とかで、次に興味を奪われがちなところです。

最近はレンタルサーバでも、MySQLなんかを開放してくれているところが増えてきたので、触ってみたい人は気軽に触れると思います。

プロセス

このあとの解説で「プロセス」という単語が出てきますが、とりあえずPerlの実行されいているプログラム1個分、Windowsで実行されてメモリにロードされているアプリケーション1個分のことだと思ってください。Windows2000やXPでタスクマネージャを起動するとプロセスタブに並んでいるアレのことです。

スレッド

スレッド(thread)とは、プロセスの中で、さらに複数の処理を同時に実行するための仕組みです。単純なPerlの掲示板などでは1プロセス=1スレッドなので考える必要ありませんが、ちょっと複雑なアプリケーションでは1プロセス=10スレッドで動いているなんていうこともあります。10個の別のプログラムを同時に動かしながら1つの大きな目的を達成するようなものです(Perlでもスレッドを扱えるそうですが筆者がやったことがないので割愛します)。 この記事の大部分では、プロセスとスレッドを混同して、どっちの意味でも「プロセス」とか書いてあったりしますが、分かりやすさのためなのでご了承ください。

更新履歴

2009.11.23
書きっぱなしで放置していましたが、筆者の知識不足で明らかな書き間違いがあった点を削除し、一部の記載を修正しました。

謝辞

公開するにあたり、以下の方々の協力を頂きました。ありがとうございました。

排他制御とは何をするためのもの? - 目的を理解する

では本題に入りましょう。

まずは、一番単純なCGI+SSIのアクセスカウンタで、問題を検証していきます。Webサーバによって起動されて、単純に数字を1ずつ加えて報告するだけのプログラムです。

プログラム1…果てしなく間違ったCGIカウンタ

#!/usr/local/bin/perl
open IN, "<counter.txt";  #ファイルを読み込み専用で開く
$count = <IN>;            #ファイルから1行読み込んで$count変数に代入
close IN;                 #ファイルを閉じる

$count++;                 #数字を1増やす
print "Content-type: text/plain\n\n$count"; #カウンタの値を出力

open OUT, ">counter.txt"; #ファイルを書き込み専用で開く
print OUT $count;         #ファイルに1行書き込む($countの内容を)
close OUT;                #ファイルを閉じる

PerlでCGIを作ったことのある人なら何の困難もなく読めると思います。CやJavaやDelphi言語やVBのプログラマさんは、申し訳ありませんが適宜頭の中でfopenやらAssignやらcout<<やらprintfやらwritelnに置き換えてお読みください。

要するにファイルを開いて1行読み込み、1加えて改めて同じファイルに書き込む、という、それだけをするプログラムです。

さて、このプログラムを1度実行し、2度実行し…とやっていくと、確かにcounter.txtというファイルに記入された数字は、実行するたびに1ずつ上がっていきます。正しくカウンタとして動作しているように思えます。

でも、今までにCGIを作ったことがあって「ロック」という単語を聞いたことがある人なら、さすがにこのプログラムは「ヤバい」と感じるかもしれませんね。 実際にこのプログラムを1日100万アクセスのサイトのカウンタとして採用すると、まず間違いなく、恐怖の「ログ飛び」が発生します。1日300アクセスでも本当に運が悪いと飛ぶでしょう。いつの間にかデータファイルであるcounter.txtが『破壊され』、カウンタが1に戻ってしまう、という恐怖の現象です。

さて質問。

何故、「恐怖のログ飛び」は発生するのでしょうか?

こう質問すると、よく単純な答えとして返ってくるのは、

「同時に2つのプログラムがデータファイルにアクセスするから壊れるんだよ」

…というものです。そう聞いて、何となく「なるほど」、と思って、 とりあえずそう理解している人もいるんじゃないでしょうか。

「同時に2つのプログラムがデータファイルにアクセスしようとすると、データファイルは壊れる(ことがある)」「だから同時に1つのプログラムしかアクセスできないようにする機構が必要。」「それがロック処理なのだよ」

少なくとも、間違った事は書いてません。「ロック」自体を聞くのが初めて、ロックの目的って何だろう? …という人は、まず上の文章を頭にたたき込めば、ロックの1割くらいは理解したことになります。

WindowsでもUNIXでもいいんですが、近代的なOSでは、複数のプロセス(正確にはスレッド)が1つのOSの中で同時に動いています。難しい言葉ではプリエンプティブマルチタスクOS、などと言います 。当たり前過ぎて意識しないかもしれませんが、Wordでレポート作成中にソリティアが出来るのも、Windowsが 、1つしかないCPU(2つかもしれませんけど)で同時に2つ以上のプロセスが実行できるよう、陰で工夫しているからです。

CGIも一緒で、ほぼ同時にサイトへのアクセスがあると、ほぼ同時にCGIが2個起動して、ほぼ同時に同じファイルを処理しようとします。

さっきのプログラム。

open IN, "<counter.txt";
$count = <IN>;
close IN;

$count++;

open OUT, ">counter.txt";
print OUT $count;
close OUT;

う~~ん確かに、同時に同じファイルで何か読み書きしようとしたら、いかにもまずそうだ、とは思います。なんとなく(?)ファイル壊れそうです。何が起こるか分かんない気がします。

そこで登場する排他処理とは、「俺が今からファイルに触るから、その間自分以外の奴は触るな」と他のプロセスに対して宣言すること(=ロックすること)に相当します。

宣言されている間は他の プロセスはファイルに触っちゃいけません。「終わったぜ」と言われるまで待たないといけません。トイレみたいなもんだと思いましょう(唐突)。10人が同時に駆け込もうとしても、中に入って鍵を閉めることができるのは常に1人。

ファイルを読み書きする前には必ず「入ってま~す」という目印を立てて、用を済ませたら「空いてま~す」という目印をどこかに立てておく、これをプログラムで実現しましょう。そうすれば、他の人(プロセス)は、「入ってます」になっている間は順番を待ち、「空いてます」になってから自分が入ればいいわけです。このようにして同時に複数のプロセスが同じファイル(ファイル以外でもいいですが)にアクセスするのを防ぐ、これが「排他処理」、他の人を排する処理なわけです。

ここまでのまとめ

複数のプロセス(スレッド)が同時に同じデータを処理しようとすることが破壊の原因。

排他制御の基本は「順番待ち」。同じデータに複数の人がアクセスする場合には、「自分以外の人は立ち入り禁止」な区域に入る前に、「入ってます」という目印を立てる。

ただ、 ここまでの説明は日本語のサイトを検索するだけで、ごまんと(誇張)見つかります。 ていうか、この文章を読んでる人のほとんどは、ここまでの話くらい、ご存じだろう、と推察します。

だけど、これだけで正しい排他制御ができるようには絶対なりません。次に進みましょう。

実際にどうしてファイルが壊れるのか? - 下手なロックは意味がない

排他制御の目的は分かりました。

が、 今までの文章を読んでどう思ったでしょうか。

「コンピュータって賢いと思っていたけど、同時に同じファイルにアクセスしようとするとランダムにファイルの中身が壊れちゃうのか、案外怖いもんだな」でしょうか。「同時にアクセスすると壊れるっていうけど、具体的にコンピュータの中で何が起きてるんだろう」でしょうか。 そう思ってくれたなら、この文書を書いた甲斐があるというものです(笑)

こういう心配はもっともですが、ご安心下さい。実際のところ、OSとファイルシステムはちゃんと堅牢に動いています(少なくともUNIXとWindowsNT系のファイルシステムでは)。どんなに一生懸命同じファイルに同時にアクセスしようとしても、それが原因で ディスクが読み取り不可能になったりOSが止まったりすることはありません。ファイルのリネームを10万回繰り返したってファイルが分裂したり消えたり、実体のないファイルができたりもしません。そういう最悪の事態はOSがちゃんと防いでいます。

あと 「そんなにファイルを同時に触られるのがイヤだったら、UNIXやWindowsが自動的にロックして、2人目以降のファイルのopenではエラー出すようにしてくれりゃいいじゃないか、何でわざわざ 、自前でロックなんて実装しなきゃいけないんだ」と思うかもしれません。が、そういうわけにもいきません。そもそも2人が同時にファイルを「読み込む」のは、何の問題もありませんし、プログラマが 上手に制御すれば、ファイルを同時に「書き込む」のも問題ではありません。実際にDBシステムでは、1個のデータファイルが凄い勢いで 数十個のスレッドによって読み書きされ たりしますが、DBシステムを作ったプログラマが偉いので、データは壊れたりしません。Windowsのレジストリだって1つの大きなファイルですが、知ってか知らずか複数のアプリケーションが同時にアクセスしまくっていますし、問題は発生しません。

だから、「自分のプログラムではどこに排他制御が必要か」を考えて、実装しないといけ ないのは、そのプログラムを作っている貴方なのです。OSの動きを分かっていれば安全なプログラムは作れますし、分かっていなければいつまでたっても正しい排他制御は実現できません。

動かないロック処理

前置きはこのくらいにして、実際のプログラムを。

「入ってます」の目印に、とりあえず普通のファイルを作ってみて、その存在の有無で判定することにしましょう。いわゆる「ロックファイル」を使ったロック方法です。このロックファイルは、「入ってます」の目印専用のため、中身はなく、単純に存在の有無だけが重要なファイルです。

プログラム2…とりあえずロックファイルを作っただけの例

#!/usr/local/bin/perl

while  (-e "lockfile.lock") {sleep(1);} #ロックファイルが存在する間、1秒ずつ待つ
open LOCK, ">lockfile.lock"; #ロックファイルがないので作る、ここから先は我が天下
close LOCK;


open IN, "<counter.txt";
$count = <IN>;
close IN;

$count++;
print "Content-type: text/plain\n\n$count";

open OUT, ">counter.txt";
print OUT $count;
close OUT;

unlink("lockfile.lock"); #ファイル削除、次の方どうぞ~

追加した部分は示してあります。Perlが分からない人のために述べると、"-e"はファイルが存在するかどうかを判定して真偽値を返す演算子、unlinkは単純にファイルを削除する関数です。sleep(1);で1秒間プログラムの実行を停止します。

追加した部分の意味は大丈夫でしょうか。「lockfile.lockというファイルが存在する間は待つ」「ロックファイルが存在しない場合にロックファイルを作成する(新規作成モードで追加してすぐ閉じる)」「実際のデータファイルを処理してから」「ロックファイルを削除しておく」ですね。

問題がないように思えるでしょうか。

実は、これでも、しっかりカウンタは飛びます。大事な数字がリセットされてしまいます。実際、1日12000件のアクセスがあった某サイトでは、こういうカウンタを使っていたら、2日間で3回も カウンタが飛んじゃったそうです(まぁ運も悪いけど)。カウンタとしての役目を果たしてませんね(笑)

何が悪いのか、分かるでしょうか。…といいつつ、排他制御全く知らない人にこれを見せて、何が悪いか自分で思いついて指摘できる人はほとんどいないんですが…。やる気があれば、ここで10分くらい悩んでみてください。実質12行の短いプログラムですし。

解説

何故これで数字がリセットされるのか考えましょう。プロセスAさんとプロセスBさんがいると考えてください。2人とも、WebサーバへのアクセスによってCGIとして起動されたPerlのプログラムです。偶然、ほとんど同じ時刻に起動されました。

まずAさんが、ほんの一瞬、例えば0.01秒だけ先に起動しました。まずはPerlの記述の順番に従って、ロックファイルを探しましょう。ロックファイルはこの時点で、存在していませんでした。Aさんは、「ラッキー、空いている」と思って、処理を次の行に進めます。次の行ではロックファイルをさくせ…

この時点で、あろうことかAさんは眠ってしまいました

UNIXやWindowsのようなOSでは、CPUは少数個(1個のことも多い)しかないのにプロセスはたくさんあるので、OSが、全てのプロセスに順番にほんの短い間だけCPU処理時間を割り当てては奪い、ということを繰り返して、見た目に同時に実行されているようにしています。プロセスは、黙々と自分の仕事をしているように思いこんでいますが、実はOSによって非常に短い時間の間に、眠らされて起こされて、を繰り返しています。

Aさんが眠っている間に、0.01秒だけ遅れて起動したBさんが実行を進めます。Bさんも同じように、ロックファイルを探そうとします。ロックファイルはこの時点で、まだ誰にも作成されていません。Bさんは「空いている」と思って、処理を次の行に進め、実際にロックファイルを作成します。Bさんは自分でロックを行ったので安心して本物のカウンターのデータファイルを開きます。データファイルには99999と書かれていました。Bさんは「よっしゃキリ番」と思った…かどうかは定かではありませんが、とにかくその中身を読み取って1加え、「次は100000をファイルに書き込もう」と考えて、データファイルを開きます。open OUT, ">counter.txt"; が実行された時点で、データファイルは上書き用に新しく作成されて、counter.txt は、新しい空っぽのファイルとなります。もちろんその直後で正しい数字を書き込むつもりです。

…ところがこの時点で、今度はBさんが眠ってしまいます。代わってAさんが実行を再開します。Aさんはどこまで進んでましたっけ。確か、「ロックファイルは存在しない」と判断した直後で気絶していたのでした。そこでAさんは、その後何事もなかったかのように、自分もロックファイルを作成しようとします。open LOCK, ">lockfile.lock"; は、既存のファイルがあっても何のエラーも出さずに、ただそれを上書きして成功します。Aさんは、Bさんが作ったロックファイルを上書きしつつ、それに気づかずに次の処理を進めます。

次の処理はデータファイルから読み取って1を加えることでした。Aさんはデータファイルを開きます。するとそのデータファイルcounter.txt空っぽでした。何しろBさんがさっき自分用に新しく作ったファイルなので。何も知らないAさんはそれを見て、「じゃあカウンタはゼロだ、次は1を書き込もう」と考えます。…もう悲劇はだいたい想像がついたと思います。

…ここで再びBさんが復活、Aさんは意識不明となります。Bさんは「次は100000を書き込むぞ」と思ったところで気絶していたので、実際にファイルに「100000」と書き込んで、データファイルを閉じ、ロックファイルを削除して終了します。Bさんお疲れ様でした。

最後にAさんは再び目が覚めて、「100000」と書かれたデータファイルを上書きして颯爽と「1」と書き込みます。そしてその後ロックファイルを削除するときにはロックファイルは本当は存在していないかもしれませんが、そこで不審がっても今更しょうがないので、そのまま終了します。Aさんお疲れ様でした。

以上、最後に残ったデータファイルには「1」と書かれています。めでたしめでたしでカウンタデータが飛びました

……こんな凄まじい偶然、本当に起こるんだろうかと思いますか? 混雑しているサーバで、1時間に3600件(毎秒1件)のアクセスがあれば、相当な確率で起こります。 あなたが配布を考えている こんなカウンタCGIがそこそこの人気サイト100か所で使われたとしたら、今日もどこかでカウンタが飛んでいますよ、の勢いです。

何が悪かったのか?

まずは何より、今までの一連の動作で、本当に「同時」にファイルに書き込んでいるわけじゃないことに気を付けてください。 コンピュータなんて所詮単純作業の機械。OSは何も知らずに、単純にAさんとBさんを交互に起こしている「だけ」です。

AさんとBさんは、単純に交互に目覚めてはファイルの存在をチェックしたり、ファイルを作ったり、ファイルに書き込んだりしている「だけ」です。全く同時にアクセスする、ということは実際にはあり得ません。OSが仕様通りにうごいているんだから、悪いのはAさんBさんを作ったプログラマの方です。明らかに共犯で、ファイルを自分達で壊して知らん顔しているわけです。こんな実装をしてデータが飛んでも不思議でもなんでもありません

最も想定外だったのは、ロックファイルを作っているはずなのに、そこのチェックをすり抜けていることでしょう。AさんもBさんも、同じ時間に起動しておきながら、平然とふたりしてロックファイルの壁を突破しています。

修正してみる

では、上のプログラムを、データが飛ばないように、思う存分自分で修正してみてください。いろんな案が出てくるでしょう。

  1. ロックファイルを作成したり削除したりする代わりに、ロックファイルの中に自分の名前「A」とか「B」とか(具体的にはプロセスIDとかプロセスハンドル)を書き込む
  2. ロックファイルを作成したりする代わりに、Windowsのレジストリとか、OSの共有メモリとかを使う
  3. open OUT, ">counter.txt"; すると一瞬ファイルが空になるので、open OUT, "+<counter.txt"; して、seekを使って上書きで書き込む
  4. 読み込んだ時にファイルが空っぽだったら怪しんでエラーを出してそこで終わる
  5. 別の一時ファイルに書き込んでおいてから、後で本物のデータファイルの名前にリネームする
  6. バックアップのカウントデータを3つくらいのファイルにとって、3か所から読み取って多数決でカウントを決める(笑)

6を除いて、実際に筆者はこれに近いものを見てきたぞ、というのばっかりです。

残念ながら 1番目2番目の方法は、ほとんど全く同じ仕組みによってロックの仕組みが突破され、データが失われてしまいます。無力ですねえ。

まぁこの程度のカウンタなら3~5番目の姑息的な方法が役に立つ「かも」しれませんね。ただし、よく考えていけばわかると思いますが、これらの方法だとAさんBさんのどっちかはきちんとカウントしないままに終わってしまうでしょう。

3や5の方法だと、きっとAさんもBさんも同じ数字「100000」が表示されてしまい、どっちがキリ番を取ったかで揉めてしまうような素敵カウンタが生まれてしまうことでしょう。キリ番直前にみんなで一斉にリロードしたおかげで、掲示板にキリ番報告が4人並んだとかいう逸話も知っています(ちゃんとリロードで回るカウンタで)。

4の方法だと、AさんとBさんのどっちかは数字を取得できなくてカウントアップもできないという、無敵カウンタになるでしょう。何故そうなるか、ある程度納得し て、自分はこんなミスをしたくない、と誓ったら次に進んでください。(6は論外♪)

…まあ残念ながら世の中にはこういう怪しいカウンタは多数存在します。本来カウンタで同じ数字が2つ出てきたり、数字が出てこなかったりしたらとんでもないことです。「OSとロックの仕様の限界でしょうがないんですよ」という名言を聞いたことがありますが、そんな人に会員番号の発行プログラムとか、コンサートチケットや座席番号発行とかのアクセスが集中する業務プログラムを委託しちゃったら怖すぎます。こういう大規模サイトも大抵、UNIXかWindowsのどっちかで動いてます。

姑息的方法はやめましょう。これが掲示板だったらどうなるか? 書き込んだ記事が失われます。掲示板ならまだいい、オークションシステムや在庫限定の通販システムだったらどうなるか? 「あなたが最高入札者です」とか2人に表示されてしまったら、10個限定のプレミアグッズの注文を11人から受けてしまったら、お金が振り込まれた後で客のデータが消えたら…後で訴訟問題になりかねません。

「正しくロックする」ことを学びましょう。

ここまでのまとめ

中途半端な知識でロックの真似事をしたり、中途半端な手段で修正しようとすると、火傷する。

正しい鍵のかけ方

ロック専門の関数

ここまでで、Perlの中だけで適当にフラグを立ててロックを実現しようとすると、大抵が破綻することは分かりました。

「入ってませんか?」とノックして、空いてることを確認したのにね。トイレで言えばノックした後に他の人に目にも留まらないスピードで駆け込まれた、という状況(笑) 個室の中で2人がご対面。

困った。

そこで登場するのが、UNIXとWindowsNT(2000, XP)で動くPerlの関数 flock です。

flockは、OSが準備しているファイルロック専門の仕組みを利用します。OS提供なので安全確実です。

え? 最初からこんな便利なものがあるんだからこれを説明しろって? …それやると理解しないままになっちゃうじゃないですか(笑) それに、Perlのflockは使い方がちょっと覚えづらいので敬遠されがちでして、存在は知っていても敢えてロックファイルなどの不完全メソッドを使う、という人もいます から、まずロックファイルを否定しておきたかったと。

flockが他の方式と何が違うか、というと、「入ってませんか?」というチェックから「私が入りました」という宣言までを、誰にも邪魔されずに一発でやってくれます

というよりはむしろ、入っていますよ宣言を自分でやる代わりに、時の番人であるOS(UNIXとかWindows)に頼む、と思ってください。OSはそもそも、すべてのプロセスを生かしたり殺したり休ませたりしている張本人であり神様です。その神様が、下々のプロセスAさんやBさんからほぼ同時に「私にロックさせてください」と頼まれたとしても、神様は絶対にどちらかにしかロックを与えません。

その結果、flockでのロックに成功した場合、AさんやBさんは絶対に他の人が先にロックしていない、と自信を持てるわけです。

それじゃ、とりあえずこういうプログラムを書いてみましょう。

プログラム3…flockを使ってみた例

#!/usr/local/bin/perl

open IN, "<counter.txt";
flock IN, 1; # 共有ロックが得られるまで待つ
$count = <IN>;
close IN; # ロックは自動的に解除される

$count++;
print "Content-type: text/plain\n\n$count";

open OUT, ">counter.txt";
flock OUT, 2; # 排他ロックが得られるまで待つ
print OUT $count;
close OUT; # ロックは自動的に解除される

flockの使い方はPerlのマニュアル参照…にしたいところですが、ついでに大事な概念も述べちゃいましょう。使い方や数字の意味はPerl固有のものですが、「共有ロック」「排他ロック」の違いはしっかり認識してください。

flockの使い方は以下の通り。

flock FILEHANDLE, OPERATION

FILEHANDLEopenで開いたときに使ったファイルハンドル、OPERATIONは1,2,5,6,8のいずれかです(定数も宣言されていますので適当なマニュアル参照)。

排他ロック(exclusive lock)は、「俺がファイルを使うから、俺が終わるまで誰も触るな」という意味のロック(これまで説明していたロック)です。共有ロック(shared lock)は、「俺が読み込むから、他に同時に読み込みたい奴等は勝手にしてくれ、ただし書き込みたい奴は俺が終わるまで待て」という意味のロックです。共有ロックのことを読み取り専用ロック、リードロックとか言ったり、排他ロックのことを書き込みロック・ライトロックとか言ったりもします。

普通は同時に同じファイルを読み込むことには危険もないので、共有ロックに意味がないように思え た人がいるかもしれません。が、共有ロックの後半「ただし書き込みたい奴は俺が終わるまで待て」が重要です。共有ロックを使えば、自分が読み取っている最中に他のファイルがそのファイルを変更する、ということを避けられます。

なお、たまにflockが使えても共有ロックだけは使えない、というシステムがあるらしいですが、そういう場合はしょうがないので、常に排他ロックを使いましょう 。複数の人が同時に読み取れた方が効率はやや良いですが、まあカウンタ程度なら、排他ロックでも大した違いはありません(理由は考えて下さい)。

つまり。

共有ロックは、他の誰も排他ロックを取得していない場合(共有ロックなら他のプロセスがいくら取得していてもいい)に取得できます。排他ロックは、あらゆるロックを誰も取得していない場合にのみ取得できます。

言い換えると、ある1つのファイルに対して、あり得るロック状況は、「誰もロックしていない」「1プロセス以上(1でもいい)が共有ロックを得ている」「1プロセスだけが排他ロックを得ている」の3種類です。

…。

……。

難しかったですか? 今、重要な事を言いました。共有ロックと排他ロック、いつどっちを使えばいいはずなのか、ここで考えて下さい。あとでもう一度聞きます。

さてOPERATION12の場合は、「何がなんでも私にロックをください、くれないのなら私の存在価値なんてありません」と、OSにロックの取得を依頼したあとはプロセスの実行を停止して待ち続けます(この動作をブロッキングロックと言いますが、排他ロックとかと混同しないで下さい)。逆に5や6はノンブロッキングで、「今空いてますか? だったらロックしてください、ダメなら後でまたお願いします」というものです。いずれにせよ、いったんロックを取得してしまえば後のことは変わりません。(ブロッキングロックとシグナルを使って、特定の秒数だけ「待ち続ける」方法もあります。上級編 かつUNIXオンリーなので他のサイト参照)

また、closeによってロックは自動的に解除されるので、カウンタや掲示板程度のプログラムなら、明示的にロックを解除する場面は ありません(ちゃんとロック解除したんだという自己確認のために書く人もいます)。

というわけで、この関数を使うと、1つだけのプロセスがロックを取得できるようになりました。

まだまだ甘くない

さて。

せっかくflockを使った、今の「プログラム3」には、まだバグが2つあります。

1個の間違いは、ログが飛んでゼロに戻っちゃうバグです。

もう1個の間違いは、2人以上の人に同じ数字を表示してしまうバグです。

実質10行の中にどんな問題が隠されているのか。探してみて下さい。

再掲・プログラム3…flockを使ってみた例

#!/usr/local/bin/perl

open IN, "<counter.txt";
flock IN, 1; # 共有ロックが得られるまで待つ
$count = <IN>;
close IN; # ロックは自動的に解除される

$count++;
print "Content-type: text/plain\n\n$count";

open OUT, ">counter.txt";
flock OUT, 2; # 排他ロックが得られるまで待つ
print OUT $count;
close OUT; # ロックは自動的に解除される

……(考えタイム)……

…………。

いいですか?

……答え、言っちゃうよー(笑)

1個目のバグ、ログが完全に消えてしまう原因は、open OUT, ">counter.txt";の行です。これが呼ばれた時点で、まだ排他ロックを取得していないのに、空っぽ のファイルが作成されてしまいます。一瞬ですけどあなどってはいけません。 ちゃんとPerlの説明書に、「>を付けてオープンするとファイルはクリアされます」って書いてあります。

Bさんが排他ロックを得ない間にファイルを空っぽにしてしまえば、Aさんが共有ロックを得て読み出した値は、偶然やっぱり空っぽでした、という事態が起こりえます。 空っぽのファイルを読み取ってしまったAさんは、Bさんの仕事が終了した後に、何も知らず「1」というファイルを作ってしまいます。

実際にあるんですよこんなミス。本人はロック完璧にやってるつもりだから疑わないし。

2個目のバグ。

前半の1回目のファイルオープンでは読み取るから、と「共有ロック」を使っていますが、これでいいでしょうか? ダメですね。これだと2つのプロセスが 同時に共有ロックを得て、同じ数字を読み取ってしまう可能性があります。同じ数字が2か所で表示される情けないカウンタ。

でもこのパターン、よく見ます。読み取りは共有ロックだ、と信じているパターン。「掲示板で記事表示する」ためにデータファイル読み取るなら、共有ロックでいいかもしれません。でもカウンタの数を読み取るのに共有ロックを使うというのは、二重カウントしてくれと言っているようなもんです。

さらに修正

上記の問題をすべて修正したのがこちら。

プログラム4…やっとまともになった「単純なカウンタ」

#!/usr/local/bin/perl

open INOUT, "+<counter.txt"; # 読み書き両用でオープン
flock INOUT, 2; # 排他ロックが得られるまで永遠に待つ
$count = <INOUT>;

$count++;
print "Content-type: text/plain\n\n$count";

seek INOUT, 0, 0; # 書き込み場所をファイル先頭に巻き戻し~
print INOUT $count;
close INOUT; # ロックは自動的に解除される

プログラム3より短くなりましたね。ロックは行数食うから面倒だと思ってる人がいますが、そうでもないです。

ファイルは1回だけ「読み書きモード」で開いて、排他ロックを得てから読み書きします。プログラムの作業途中なのにファイルを1回閉じてしまうのは、「ファイル閉じたから他の人使ってくれ」と言ってるようなもんです。

seekは、ファイルの読み取り位置を巻き戻したり進めたりする関数です。いったん$counter = <INOUT>で読み取った後は、ファイルの中の位置(読み書きのためのカーソルみたいなものだと思いましょう)がファイルの末尾になっちゃっているので、巻き戻してあげます(カセットテープにデータを入れていた人は、頭出しみたいなものだと思えば理解しやすい)

ともあれ、これでやっと、理論的には壊れないカウンタが完成しました。

実際は、ロックが得られるまで永遠に待たれるとCGI的には都合が悪いので(ロックしたまま暴走しているプロセスがある場合など)、すぐ諦める方のロックで試して、ダメだったら1秒ごとに最大5回挑戦し、それでもダメならエラーを出して終了、とか、そういう機構を組むこと もあります。CGI特有の話になってくるので、実際のところは有名どころのCGIで勉強してください(有名だから完璧だとは限りませんが…)

flock以外の代替案とその問題

flockは、UNIXの中でも使えないOSがある、という問題があります。Windows版のPerlを使っている場合には、WindowsNT、2000、XP以降である必要があります。ファイルロック自体がOSの機構 ・システムコールなので、それが使えるかどうかもOSの機能に依存するわけです。

それじゃ困る、最大の互換性が必要なんだ、という場合には、Perlのmkdirコマンドを使う方法があります。mkdirは、新しいディレクトリ(フォルダ)を作るというのが本来の目的の関数です。OSにとって、2つのプロセスから同時に「ディレクトリ(フォルダ)作って」と頼まれた場合に、「はいはい」と平気で2つの同じ名前のディレクトリを作ったらファイルシステムが破綻します(同じ名前のディレクトリが2個!)。なので、OSはこういう場合は、ロックと かと同様、どっちかのプロセスの言うことを聞いてディレクトリを作って、もう片方からのリクエストに対してはエラーを返 してくれます。これを使い、自分でディレクトリを作成できた場合に「入ってます」宣言状態、ということにして、最後にディレクトリを削除すればいいわけです。(プログラム2の、openを使ったロックファイルと何が違うか比較してみてください)

他にも、symlinkを使う方法(UNIXのみ)などがありますが、この辺になってくるとロック博物館の様相を呈してきます し、いろんな人が語っていますので、他のサイトを参考にしてくださいませ。

個人的には、「OSがそのために準備してくれている機能」を使わない、というのも気分が悪いので、flockを推奨しています。 自分しか使わないようなスクリプトなら、flock以外を使う理由はありません。超細かい話ですが、WindowsとUNIXではmkdirでいけるものの、他のOSでもmkdirで安心できるとは限らない(ドキュメントに書いてない)でしょうし。symlinkについても、OS環境によっては稀にきちんとロックしてくれないとか書いてあるサイトもあります(ハッキリ言って興味ないので、自分で確認していません)。

暴走やエラーの際の優劣

本題と無関係ですが、「ロックが残る」というのは多くの人が困っている問題でしょうから、ここで少しだけ比較して触れておきます。

ロック専門関数flock以外の自前ロックを使った場合、OSが自動的にロックを開放してくれる、なんていう素敵なことはやってくれません。自前でロックを解除する必要があり ます。バグのあるCGIがロック解除し忘れたまま終了した場合、延々と他のプログラムが目的ファイルに触れなくなったりします。なので よく、10分前より古いロックが存在したら、それはロック解除忘れだと判断して無条件にロックを削除して処理を続行する…なんていう処理を追加することがあります。 もちろん、どんなエラーも感心できるものではありませんが、「あるプロセスが些細なエラーで止まると、他のプロセスも動かなくなる」なんて、そんな対応、考えるの面倒くさいよね。

一方でflockを使った場合、プログラムがInternal Server Errorとか出して止まっても、自動でロックが解除されます。ラッキー。ただし、プログラムが「暴走」した場合、つまり正しい動作もせずエラーで終了もしない無限ループに陥った場合に 限っては、延々と他のプログラムがファイルに触れなくなります。

あくまで個人の経験ですが、CGIって入力の例外とかがいっぱいあってエラーの分岐は多いけど、無限ループや暴走ってそう簡単には起きない じゃないですか。スペルミスとかしてInternal Server Errorとかを見た回数と、間違って無限ループ・暴走させた経験、あなたの中でどちらが多かったか思い出して下さい。

というわけで、エラー周りでも、やっぱりflockの方が優秀だと言えるんじゃないでしょうか。大体CGIプロセスが無限ループっていうのはよっぽどの大バグでロック とか以前の問題ですから(笑)、ロックが残った場合の処理など考えなくても良いくらいです。しかも最近は、大抵の商用レンタルサーバ で、2分とか暴走してるCGIを自動的に停止する機能とか備えていると思うので、その時にロックも解除されます。

一方、自前ロックだと、ちょっとした分岐ミスや例外処理抜けでロックが残ると、誰も気づいてくれないことが。

以上から、使えるならflockが大推奨なんですが、まあそれでも、flockがそもそも使えないOSがあるんじゃ、しょうがない。不特定多数に配布するCGIを作っているなら、以上のような性質を理解した上で、flockmkdirからオプションで選べる、とか、そうしとくのがいいんでしょう。これ以上先の具体的な実装方法や実践、flockevalしようだの、mkdirの自前ロックを解除するときのコツだのの話については、他のサイトがたくさんあるので、そちら参照しておくれ(笑) 一応このページの末尾に参考になるリンクを1個挙げておきます。

いずれにせよ、この記事のフォーカスはそれ以前の部分だということでご理解くださいませ。

ここまでのまとめ

長かったでしょうか。お疲れ様でした。

でも、ここまでの内容は、探せばときどきWebで見つかっちゃったりしますね。まじめに検索していればそれなりに解説しているサイトは(少ないですが)ありますし、PerlとCGIに限って懇切丁寧に述べているサイトなんかもあったりするので、そっち見た方がわかりやすいかも、みたいな気すらしますね 。でも、原理についてこれだけ述べた人は滅多にいないと思いますので許してください、ということで。

実証

ここまでの議論を実験で確かめておきましょう。

方法

最終的な検証は、実際に動かしてみるしかありません。そうはいっても、検証の対象は、0コンマ1秒未満の微妙なかねあいで成否が決まるプログラムです。適当にブラウザのリロードボタンを連打しまくるとかじゃ先に指が折れます。

というわけで、以下の実験は自前の自宅Webサーバ(何をやっても怒られない)と、自前の大量HTTPリクエスト同時発行プログラムを使って行いました。っていうかHTTPリクエスト同時発行プログラムは別名DoSプログラムですので、巷に多数存在するロックが不完全なCGIに向けて発射なんぞしようものなら簡単にデータ飛ぶでしょうし、ロックが完全だったとしても負荷のせいでプロバイダから契約解除されたり不正アクセスで逮捕されたり、さもなくばサイト管理人様から生ゴミ が届いたりしそうな代物です。犯罪に荷担するのはイヤなので公開は控えますがご了承ください。どうしても必要な場合は理由と、サイト管理者さんの場合はサイトのURLなどの身元情報も添えて、メールで個別にご相談ください。

自宅Webサーバのスペックは、Celeron 366MHz, Memory 192MB, Red Hat Linux 7, Perl 5.8, Apache 1.3.27です。またリクエストを発行するためのWindowsパソコンのスペックは、Celeron 2.6GHz, Memory 1024MB, Windows XP Professionalです。2つのマシンの間は、単なるスイッチングハブを通じて100MのLANで接続されています。

実験では、100個のリクエストをほぼ同時にWebサーバに投げ(これが1セット)、1セットを何度か繰り返していきました。

結果

プログラム1(対策ゼロ)では、100回のリクエスト(1セット)だけで20回カウンタが飛んでいます。 さすがに対策ゼロだと論外の成績(笑)

プログラム2(ロックファイル)では20セット(2000リクエスト)で、最終の数字が2000ではなく、1948を指していました。時々、「1」とか「2」といった不可解なカウントが出てきました。22セット目(カウント2201~2300)で、遂にカウンタが飛んで、ゼロにリセットされてしまいました。

プログラム3(flockの使い方違い)では、1セットだけで9回ほど飛びました。プログラム1と同様、論外の成績。

最後にプログラム4で、100回リクエストを100セット繰り返したところ、見事に、ちゃんと10000でした。これだけの負荷でも問題なく動作する理想のカウンタが完成です(なお、100件全部から応答が返るまでの時間は平均2.5秒で、他と比べて特に遅くはなかったことを付け加えておきます)。

考察

プログラム2ではシミュレーションの通り、完全にファイルが飛ぶためには偶然が2回重ならないといけないので、比較的「完全にとばす」のは難しいようですね。ロックを得ていない大量のプロセスが平気で空ファイルを読み取れてしまうプログラム1や3と違い、2では、一応形なりに存在するロックのお陰で、実際に同じファイルに殺到するプロセスの数が比較的小さく保たれているのでしょう。 詳しい状況は考えてみて下さい。

ファイルは1度だけ開く

議論は重なっちゃう部分もあるのですが、ここまで読んでくれた人のために、もう一度、非常に多くのCGIプログラムが犯している間違いを指摘しておきます。

みなさんは、

「ファイルが壊れないようにするためには、ファイルを出来るだけ短い時間で読み込んですぐ閉じ、書き込むときも出来るだけ短い時間で書き込んですぐ閉じるようにする、そうすれば比較的安全♪」と、思っていませんか?

大間違いです。

いわゆる「掲示板」プログラムの場合。

  1. 掲示板ログファイルを読み取り用にロックして開く
  2. 掲示板データファイルの中身を読み取る
  3. いったんファイルを閉じる
  4. 日付やらタグ除去やらの諸々の処理をする。場合によってはここで数百行
  5. 改めて読み取り・上書き用でファイルを開く
  6. ファイルを排他ロックして
  7. ファイルに書き込む
  8. ファイルを閉じる(ロックは自動的に解除)

上のような動作をするCGIプログラムがヤケに多いんですが、これがどれほど危険か、ここまで読んだ方なら分かるはずです。

例えば。

プロセスAさんとプロセスBさんがほぼ同時に起動してログデータを開いて、ほぼ同じ時点でのログデータ(同じ内容)をメモリに読み込みました(12時00分00秒00としましょう)。Aさんが花子さんの記事を追加したログファイルを(きちんとロックして)書き込み、ロックを解除しました(12時00分00秒05)。Aさんがログファイルの書き込みロックを解除するのを待っていたBさんは、太郎さんの記事を書き込みました(12時00分00秒10)。その際に花子さんの記事は上書きされて消えてしまいました。花子さんの記事は、0.05秒の間しかこの世に存在できなかったということですじゃ。南無。

この間違いは意外と見過ごされています。なぜなら、とにもかくにも掲示板のログが「完全に消失」ということは起こらないからです。読んだり書いたりしている真っ最中はロックが効いていますので、書式的に破壊されたファイルが作成されることはありません。せいぜい、最後の書き込みが反映されてません、とか、消したはずの書き込みが復活しました、とか。その程度なら誰もバグ報告しないから、CGIを作った本人も気づかなかったりして。だけどバグはバグ、データ消失はデータ消失。

こんな誤動作を防ぐためにはどうすればいいでしょうか? 簡単です。ファイルを読み込んでから処理を行ってファイルを書き込むまで、他の誰にも触らせなければいいんですね。最初に読み込みのためにログファイルを開いた時点で、「これから自分がいじるつもりだから、他の人は読み込みもしないで 待っててね」と宣言し、最後の書き込みが終わるまで宣言し続けます。途中でファイルを閉じてはいけません(flockを使っているのならロックが解除されてしまいます。mkdirなどを使った場合に敢えてファイルを開いたり閉じたりするのは無駄処理です)。

こうしましょう。

  1. 掲示板ログファイルを読み書き両用で開く
  2. ファイルを排他ロックする(誰も触るな!)
  3. 掲示板データファイルの中身を読み取る
  4. 日付やらタグ除去やらの諸々の処理をする(何行でもいい)
  5. 開いたままのファイルに先頭から書き込む
  6. ファイルを閉じる(ロックは解除される)

ロックは1回だけ。シンプルに、かつ安全になりました。

あとでファイルに書き込みをするつもりなら、読み込む時点から排他ロックしておかないとダメです(後で書き込む直前に排他ロックに切り替えようとしても、その瞬間に別のプロセスに割り込まれて排他ロックを奪われるかもしれません)。逆に最初から表示だけしかするつもりがないのなら、共有ロックで十分ということになります。

ちゃんとした排他処理さえできていれば、「自分が処理をしている間はずーっとファイルは開きっぱなしで自分以外の誰にも触らせない」のが、データを守るためには最も安全なのです。

「データファイルが大きくなるほど、データが飛ぶ可能性が飛躍的に高まります」とか「こまめにファイルを閉じることでデータが飛ぶ危険を最小限にしています」とか言っている人のCGIは信用しないようにしましょう。2回もファイルを開いたり閉じたりすると、その間に何が起こってるか分からない、怖い! …そういう感覚が自然に沸くようになるとOKです。 一旦理解すれば、簡単なことですよね?

余談ですがこう書くと、「こまめにファイルを閉じた方がサーバの負荷は低くなるだろう」とか考える人がいます。でも、「ファイルを開いたり閉じたりする処理はそれ自体が結構負荷が高い」ですし、「ファイルアクセスしないメモリ上の中間処理は意外と速い」です。それに「高負荷の時にいかにも怪しい動作で動き続ける掲示板と、高負荷の時にも正しくデータが守られる掲示板の、どちらが優秀か」を考えれば、ファイルを不必要に閉じたり開いたりするメリットはありません。

補足 - それでもログが飛ぶ場合

flockがサポートされているOSで、flockを正しく使い、正しいロックをかけてもデータが消失する非常に稀なケースとしては、サーバの負荷が極端に高くなりすぎて、プロセスが強制終了されてしまった、とか、OS自体が暴走してしまった、とか、そういう場合が考えられます。ログファイル30MBで同時アクセス100人とかで、CPUもハードディスクも全開で、静的HTMLにもアクセスできないとかの、本当に無茶苦茶な負荷。 あるいはハードディスクの容量が限界に来た、とかでもデータが消失しますな。

これらはどうしようもありません。どうせ正常な運用にならないので、サーバ能力を物理的に増強するしかありません。このレベルになるとOS自体のいろんな機能が誤作動を起こします(笑)

逆に言えば、そういう極端な話じゃないのにログが飛んだなら、何かプログラムが間違っている可能性が高いです。

以下のような項目をチェックしましょう。

クイズ

カウンターや掲示板CGIでのデータファイルの排他処理について、正しいのはどれか?

  1. 小さいファイル(10バイト以下)なら処理は一瞬で終わるので、ロックは必要ない。
  2. ファイルが大きいと、正しくロックをしてもファイルが消えるのはしょうがない。
  3. 書き込む際にはロックが必要だが、ファイルを読み取る際にはロックは必要ない。
  4. ファイルから読み書きする際は、読む瞬間・書く瞬間だけファイルを開いて、アクセスが済んだらすぐに閉じるのが安全である。
  5. ロック処理は信頼が置けないので、ロック以外の防御方法(一時ファイルに書き込んでリネーム、など)を採用した方が安全性が高まる。
  6. flockの代わりに、普通のファイルをロックに代用すべきである。
  7. いろいろ工夫したところで、結局は運任せでしかない。

正解は「なし」です。正しい記述は1個もありませんね。

ここまで読んでくれた人、どのくらいいるんでしょうか(笑) とりあえずCGIのお話はここまでで終わりにします。

以降はデータベースの話を少しだけ。

データベースでの排他処理

はじめに

MySQLやPostgreSQLなど、一般的なデータベースシステムは、この排他処理について非常に注意深く設計されており、簡単に排他処理を実現できるようになっています。さらに、Perlで単純なファイルを扱う場合にはファイル全体をロックすることしかできなかったのに対して(たとえ1MBのファイルのたった1行を変更するためだったとしても)、DBでは自分がいじる必要がある部分だけをロックする(行レベルロック・レコードロック)ことで、同時実行能力を確保することもできます。

既に述べましたが、データベースとは、要するに、普段掲示板CGI作者さんがテキストファイルでやってるようなデータをすっごく賢く大量に保存して、一瞬で検索したり統計取ってくれたりするため専用に開発されているシステムです。

DBでも、テキストファイルのロックのときとほとんど同じ問題が発生します。そして、理解していなければデータがおかしなことになるのも同様です。一瞬で処理が終わるテキストの掲示板ログと違って規模がでかい分だけ、排他処理を考える必要性も ぐんと増してきます。

データベースを使えばデータは安全だと思っている人がいます。確かにログ消失等の極端なことがまず起こらない、という意味では正しいんですが、そういうこと言う人に限って、排他処理が恐ろしいことになってたりするんですよね。使い方と原則を理解していなければ、データベースを使ってたって問題起きるに決まってます

でも、既に大抵の問題は説明しました。ここまでのCGI編で述べた「原則」を理解していれば、DBにおける排他処理を理解したも同然です。(DBに興味があってここから読み始めた方、ごめんなさい最初から読んでください)

ここではRDBMSとしてMySQLのMyISAM形式のテーブルのみを紹介します。

MySQLの排他処理

実はMySQLは、DBとしては特殊です。高速性を追求するために、多くの機能をそぎ落とした感があります。その分、高速です。後述するPotgreSQLが足下に及びません(体感でもそう感じますが、真面目にベンチマークとったことはないので、具体的な数字についてはそれ系の比較サイトを参照してください)。PerlのCGIから入ってくる人にとっては、PostgreSQLよりも理解しやすいし扱いやすいので、日本でも急速に普及しています。

高速ゆえに、排他処理も異様にシンプルで理解しやすく、テーブルロックしかありません。おかげ で、考え方は、今までPerlのCGIのところで散々述べてきた考え方が、ほとんどそのまま通用します。

テーブル(データの集まり)のリードロック(共有ロックのことね)を得るためには

LOCK TABLES tablename READ;

ライトロック(排他ロックですね)を得るためには

LOCK TABLES tablename WRITE;

します。ロックを解除するためには

UNLOCK TABLES;

です。以上、シンプル。

これらの明示的なロックを行わない場合、MySQLはレコードを書き込んだり変更したり削除する際にその瞬間だけテーブル全体を自動的にライトロックし、レコードを読み込む際に はその瞬間だけテーブル全体を自動的にリードロックしてくれます。そこは全自動なので、同時に2つの書き込みが競合してデータが書式的に壊れたり消えたりする、ということは起こりません。CGI編との対応で言えば、ファイルアクセスごとにとりあえずflockだけはしてくれるみたいなものです。

なのに、明示的にロックをしないといけないのは、どういう場合でしょうか? 既にCGI編の最後の方で説明した通りですね。読み取りから書き込みまでの一連の処理全体をブロックすることで、隔離度は高まってデータは安全になる場合があります。より安全かつ不整合のないデータ処理を望むのなら、ちゃんと自分の頭の中で設計して、どこでロックするべきか考えないといけません

この世界では使い古された例ですが、「郵便局のATMからお金を引き出す」という場合を考えます。10万円入っている口座から、太郎さんが10万円引き出すとしませう。この場合、プログラムのロジックは「残高が10万円残っているか?」「残っていれば10万円現金で払い出して」「残高から10万円減算しておく」です。例によって同時に引き出す人が1人なら問題はありません。ここで太郎さんと共謀した太郎さんのお父さんが、家族用に発行されたもう1枚のキャッシュカード(郵貯はこれができるんですね)を使って別のATMから、同時に10万円引きだそうと考えたとします。正午ぴったりに時刻を合わせて同時にATMを操作してみましょう。

こんな流れで合計20万円現金が出せちゃったらたまりません、こんなシステム開発した人は開発会社もろとも破滅しかねません。当然現実には不可能でしょう。こんなことのないように、MySQLを使った銀行(存在しないと思いますが)では、残高問い合わせの最初の時点から、「俺 後ではデータを書き換えるつもりだぞ」ということで、残高テーブルを明示的に排他ロックする必要があります。 自動の共有ロックなんざに頼ってられません。

これなら安全。

さて、MySQLのMyISAMテーブルはテーブル単位でしかロックできないので、300MBのテーブル全体を走査して検索するような事態が発生すると、数秒間~数十秒間、他の接続がそのテーブルの排他ロックを得られなくなって立ち往生してしまう、という問題が起きます。デザイン上の制約なので、そういう検索が起こらないよう、全てが一瞬で終わるように工夫しましょう(方法はたくさんあるのでマニュアル参照)。

どうしてもこれが避けられない場合にはPostgreSQLや、MySQLのInnoDBテーブルタイプを検討します。これらはもっと複雑な同時実行制御を備えていますが、その分理解が難しく、またシンプルな用途に使う場合には、逆に動作は低速になります(他のサイトを参照してください)。

最後に

排他制御は、難しいですが、理解してしまうとパズルみたいで面白いです。

まず、自分がファイルやメモリやテーブルなどの情報リソースに対して、どんな操作をやりたいのかを明確にしましょう。

「読み取って、処理して、書き込むまでの間、他の人に干渉させたくない」のか。「読み取りたいだけ」なのか。(アクセスログのように)「ただ1行書き込みたいだけ」なのか。今この情報を書き換えると、同時に実行するどのスレッド・プロセスが影響を受けるのか。

それが明確なら、後はパターンの適用です。頑張って下さい。

おまけ

この右下に、上記の4種類の実装方法で作ったカウンタを置いておきます。

リロードでいくらでも回るカウンタですが、プログラム4以外のカウンタは飛んでいるかもしれませんので、信用しないであげてください 。測定開始は2005年4月9日です。

ま、人気ないサイトだと関係ありませんけど(涙)

参考になるリンク

Program 1: 26439
Program 2: 26443
Program 3: 6408
Program 4: 42436