◆ マルチプロセスなことをするならファイルのロックをしたほうがいい
◆ file_put_contents はロックとってくれてないみたい
◆ popen と pclose でバックグラウンド処理する 

マルチプロセスで同じファイルを扱うコードを書いていて 困ったことになりました

ファイルを読み取ったら空になる

色々複雑ではあったのですが 関係ある部分だけ取り出すとこういうものです

[プロセス1]
$filename = "sample";
while(true){
if(is_readable($filename)){
var_dump(file_get_contents($filename));
break;
}
}

[プロセス2]
$filename = "sample";
$value = getValue();
file_put_contents($filename, json_encode($value));

プロセス 1 の方では 指定されたファイル名が 読み込み可能になるまで(ファイルが作られるまで)ループします
ファイルはプロセス 2 で作られるものです

プロセス 2 は getValue で何か値を取り出して file_put_contents でファイルを書き込んでいます


これをそれぞれ実行すると プロセス 2 の方は 書き込んだら終了
プロセス 1 の方は プロセス 2 の方でファイルが出力されたら それを出力して終わるはずです

ですが実際の結果はこれ
 

空文字です

getValue が空文字ってことはないはずなので 直接出力されたファイルを確認してみると ちゃんと書き込まれていました
また 何回か実行すると空文字のときと ちゃんとデータがあるときがありました

マルチプロセス

テスト用コード作る

表示されるデータが空だったり 入っていたりと実行ごとに違っているので マルチプロセスだから プロセス 1 でファイルが読み込み可能になって開くタイミングでは まだプロセス 2 の file_put_contents で書き込みが終わってないときがあるからじゃないかと推測

それと プロセス 1 の readable 確認と var_dump の間に適当な echo 文を入れるとほぼ 100% ちゃんと読み込めていました
echo してる間に書き込みが終わってるからに思えます

なのでこの部分だけテストしやすいように別ファイルを用意しました


[main.php]
<?php
$filename = uniqid();

pclose(popen("start /B php make.php ${filename}", "w"));

while(true){
if(is_readable($filename)){
$f = file_get_contents($filename);
echo "readable\n";
echo $f;
break;
}
}

[make.php]
<?php
$filename = $argv[1];
usleep(100000);

file_put_contents($filename, "test");

再現しない

main.php から make.php を別プロセスで実行して同じようなことをしています
(popen とかは下の方で書いてます)

別プロセスの起動方法は 上では省略しましたが同じ方法です
なぜかこれだと再現しませんでした

本当にマルチプロセスのせい?

こっちの短いサンプルだと何十回動かしてもちゃんと読み込めていて問題が起きなかったです

ですが 問題起きてる方でもファイル書き込みは file_put_contents の一回だけですし 他に理由が考えられないので マルチプロセスのせいか簡単に確認してみます

file_put_contents の前後で「#」を出力して is_redable の if 句に入った直後に 「*」 を表示するようにしてみました

[動かない方]
#*#string(4) "test"

#*string(4) "#test"

#*string(0) ""
#

問題が起きたほうだと file_put_contents が開始してから終了するまでに readable のチェックが通ってるようです
3 パターンありますが バラバラです
var_dump の出力の途中に混ざってるのもあります
file_put_contents が終わる前に var_dump の出力があるときは 読み込むタイミングでは まだデータがないようで空文字です


再現してくれない テスト用の方

[テスト用]
##*readable
test

1 つだけですが 十数回やってもずっとこうなりました
毎回 file_put_contents が終わってから is_readable の中に入っています

動かない方とくらべても file_put_contents はすでに変数にあるファイル名とデータを渡してるだけで データ量も一緒です
特に file_put_contents を待つような処理は入れてないはずです

fopen

file_put_contents は速度を変えられないので fopen にして sleep を挟んでみます
<?php
$filename = $argv[1];
usleep(100000);

echo "#";
$fp = fopen($filename, "w");
usleep(30000);
fwrite($fp, "test");
fclose($fp);
echo "#";
#readable
#

ゆっくり実行させると 書き込みが終わる前にファイルを読み込んで 空文字になってます

なぜか 問題起きてる方だと file_put_contents が遅いようです

ファイルロック

file_put_contents の速度に関係なくできるようにしたいです
並列処理で同じファイルを同時扱う時の問題といえばファイルロックでどうにかなるもの(なはず)

ということで php でファイルロックしてみます


[main.php]
<?php
$filename = uniqid();

pclose(popen("start /B php make.php ${filename}", "w"));

while(true){
if(is_readable($filename)){
$fp = fopen($filename, "r");
if(!flock($fp, LOCK_SH | LOCK_NB, $locked)){
if($locked){
echo "locked\n";
continue;
}
}else{
flock($fp, LOCK_UN);
}
echo "*";

$f = file_get_contents($filename);
echo "readable\n";
echo $f;
break;
}
}

[make.php]
<?php
$filename = $argv[1];
usleep(100000);

echo "#";
$fp = fopen($filename, "w");
flock($fp, LOCK_EX);
usleep(30000);
fwrite($fp, "test");
flock($fp, LOCK_UN);
fclose($fp);
echo "#";

make.php の方では fopen の直後に flock でロックして fclose の直前にロック解除しています
fclose してもロックは自動ではずれないので自分で解除もやらないとダメなようです

main.php では読み込み可能でもロックされているか確認して ロック中はまだ待機し続けます

ロックされてるか確認する方法を調べてみたのですが ロックされてるかどうかだけを調べる方法はないみたいで ロックを確保するときの「すでにロック済み」というフラグを見るしかないようです
そのときに ロックされていなかった時はロックを確保する必要がなくてもロックしてしまうので 手動で解除しないといけないです
ロックを解除する LOCK_UN オプションをつけた flock でロック確認すればできるのかなと思ったのですが 無理でした
ロック確保しようとするときにだけ すでにロック済みかわかるようです


結果はこんなです
#locked
locked
locked
locked
.....
.....
.....
locked
locked
locked
#*readable
test

ロックが解除されてから読み込み可能になってます
ファイルのデータもちゃんと受け取れてます

これと同じように問題があった方も修正するとちゃんと動くようになりました

file_put_contents のファイルロック

今回は fopen した後に自分で flock をしましたが file_put_contents では自動でやってくれる機能があるみたいです
コレを使ってみます
file_put_contents($filename, "test", LOCK_EX);

3 つ目の引数に LOCK_EX をつけるだけでいいそうです


ですがこれを使って上のものを実行すると空文字になることがありました
ロック中だと表示する locked の文字が出ることもなかったのでロックがされてないようです


原因はわからないですが file_put_contents のファイルロックは使えないようです

flock のタイミング

手動でやるときに感じたのですが flock はファイルポインタに対して行うので fopen した後にやるしかないです
ですが fopen したあとでロックする前に別のプロセスからアクセスされてファイルが更新されたり削除されたりしそうです

open しただけでまだ書き込みも読み取りもしてないので ロックしてからの操作は邪魔されないからそれでいいってことなのでしょうか
open したタイミングじゃなくてロックを取得したタイミングのデータが扱う対象と考えればそれでいいようにも思えます

でも 削除されたらどうするんだろう

popen pclose

マルチプロセスにするときに出ていたやつですが 説明書いておきます

やってることは コマンドをシェルから実行してるのと一緒です

普通は コマンドをコマンドプロンプトや bash とかで実行すると そのコマンドが終わるまで待ちますよね
また Linux だと最後に & をつけて Windows だと start コマンドを使うとバックグラウンド実行ができます

Windows で start コマンドを `` や exec などの通常の方法でコマンド実行すると start したコマンドはバックグラウンドで動いているのに 終わるまで php の処理が進まないです

Linux の & だとすぐに次のコマンドに進みますし Windows でもコマンドプロンプトで直接 start コマンドを実行すると すぐに次のコマンドが入力できるようになってます
ですが php から使ってると呼び出したコマンドが完了まで次に進めません

バックグラウンド処理

それを解決する方法が
pclose(popen($command, $mode));
です

プロセスをオープンしてすぐにクローズします
これだと start したのがバックグラウンドで実行されて php の処理は次に進めることができます

勘違いしそうになりますが pclose があるから次に進めてるわけではないです
開いたらちゃんと閉じようということでクローズしてるだけで popen の時点ですぐ次の pclose に進んでいます
popen で待機状態になってたら pclose 意味ないですしね

popen に start がないとき

popen のコマンドに start がないときは pclose すると中断されるのかと思ったのですが コマンドが終了するまで待機になりました
pclose されるのは popen したコマンドが終わってからになるので pclose したら中断される?という心配は無用です

バックグラウンドでもデバッグプリントしたい

この方法でバックグラウンド実行をしているとバックグラウンド側で var_dump とかしてデバッグしたい時にどうすればいいのかと思います
実は普通に echo とかするとバックグラウンドプロセスを起動した画面に表示されます

ただし 第二引数のモードを w にしておかないとダメです

popen は開いたプロセスと通信できるわけですが 通信は片方向だけです
それが読み取りなのか書き込みを選ぶのがモードです

r の読み取りを選ぶと 開いたプロセスの STDOUT (標準出力) が popen の返り値のファイルポインタ (プロセスですが php だとファイルポインタとして扱ってるみたい) になるようです
そのせいか echo しても直接画面には表示されません
popen の返り値のリソース型の値を使って取り出す必要があります

また pclose をしてしまうと アクセス方法がなくなります
ただなくなるじゃなくて echo などの標準出力への出力時にエラーが起きているようで それ以降の file_put_contents などは動作していませんでした


それと 上の方のサンプルにあったように 本来途切れないはずのところに文字が割り込んで表示されます
2 つのプロセスで var_dump のタイミングが重なるとすごいことになって ちゃんと読むことが難しそうなので そこは気をつけないとです

start でウィンドウ出したくないなら

start は普通に使うと別のコマンドプロンプトのウィンドウが出て実行されます
ウィンドウ出したくないなら
start /B php sample.php
みたいに /B をつければいいです

まとめ

マルチプロセスするときはファイルロックが大切です
file_put_contents のファイルロックは動いてないっぽいです

結局なんで問題あった方のコードだと file_put_contents が終わるより先に is_readable が通るんだろう
ファイル書き込みは数文字だけで 遅くなるようなことしてないのに