php とファイルロック
◆ マルチプロセスなことをするならファイルのロックをしたほうがいい
◆ file_put_contents はロックとってくれてないみたい
◆ popen と pclose でバックグラウンド処理する
◆ file_put_contents はロックとってくれてないみたい
◆ popen と pclose でバックグラウンド処理する
マルチプロセスで同じファイルを扱うコードを書いていて 困ったことになりました
[プロセス1]
[プロセス2]
プロセス 1 の方では 指定されたファイル名が 読み込み可能になるまで(ファイルが作られるまで)ループします
ファイルはプロセス 2 で作られるものです
プロセス 2 は getValue で何か値を取り出して file_put_contents でファイルを書き込んでいます
これをそれぞれ実行すると プロセス 2 の方は 書き込んだら終了
プロセス 1 の方は プロセス 2 の方でファイルが出力されたら それを出力して終わるはずです
ですが実際の結果はこれ
空文字です
getValue が空文字ってことはないはずなので 直接出力されたファイルを確認してみると ちゃんと書き込まれていました
また 何回か実行すると空文字のときと ちゃんとデータがあるときがありました
それと プロセス 1 の readable 確認と var_dump の間に適当な echo 文を入れるとほぼ 100% ちゃんと読み込めていました
echo してる間に書き込みが終わってるからに思えます
なのでこの部分だけテストしやすいように別ファイルを用意しました
[main.php]
[make.php]
(popen とかは下の方で書いてます)
別プロセスの起動方法は 上では省略しましたが同じ方法です
なぜかこれだと再現しませんでした
ですが 問題起きてる方でもファイル書き込みは file_put_contents の一回だけですし 他に理由が考えられないので マルチプロセスのせいか簡単に確認してみます
file_put_contents の前後で「#」を出力して is_redable の if 句に入った直後に 「*」 を表示するようにしてみました
[動かない方]
問題が起きたほうだと file_put_contents が開始してから終了するまでに readable のチェックが通ってるようです
3 パターンありますが バラバラです
var_dump の出力の途中に混ざってるのもあります
file_put_contents が終わる前に var_dump の出力があるときは 読み込むタイミングでは まだデータがないようで空文字です
再現してくれない テスト用の方
[テスト用]
1 つだけですが 十数回やってもずっとこうなりました
毎回 file_put_contents が終わってから is_readable の中に入っています
動かない方とくらべても file_put_contents はすでに変数にあるファイル名とデータを渡してるだけで データ量も一緒です
特に file_put_contents を待つような処理は入れてないはずです
ゆっくり実行させると 書き込みが終わる前にファイルを読み込んで 空文字になってます
なぜか 問題起きてる方だと file_put_contents が遅いようです
並列処理で同じファイルを同時扱う時の問題といえばファイルロックでどうにかなるもの(なはず)
ということで php でファイルロックしてみます
[main.php]
[make.php]
make.php の方では fopen の直後に flock でロックして fclose の直前にロック解除しています
fclose してもロックは自動ではずれないので自分で解除もやらないとダメなようです
main.php では読み込み可能でもロックされているか確認して ロック中はまだ待機し続けます
ロックされてるか確認する方法を調べてみたのですが ロックされてるかどうかだけを調べる方法はないみたいで ロックを確保するときの「すでにロック済み」というフラグを見るしかないようです
そのときに ロックされていなかった時はロックを確保する必要がなくてもロックしてしまうので 手動で解除しないといけないです
ロックを解除する LOCK_UN オプションをつけた flock でロック確認すればできるのかなと思ったのですが 無理でした
ロック確保しようとするときにだけ すでにロック済みかわかるようです
結果はこんなです
ロックが解除されてから読み込み可能になってます
ファイルのデータもちゃんと受け取れてます
これと同じように問題があった方も修正するとちゃんと動くようになりました
コレを使ってみます
3 つ目の引数に LOCK_EX をつけるだけでいいそうです
ですがこれを使って上のものを実行すると空文字になることがありました
ロック中だと表示する locked の文字が出ることもなかったのでロックがされてないようです
原因はわからないですが file_put_contents のファイルロックは使えないようです
ですが fopen したあとでロックする前に別のプロセスからアクセスされてファイルが更新されたり削除されたりしそうです
open しただけでまだ書き込みも読み取りもしてないので ロックしてからの操作は邪魔されないからそれでいいってことなのでしょうか
open したタイミングじゃなくてロックを取得したタイミングのデータが扱う対象と考えればそれでいいようにも思えます
でも 削除されたらどうするんだろう
やってることは コマンドをシェルから実行してるのと一緒です
普通は コマンドをコマンドプロンプトや bash とかで実行すると そのコマンドが終わるまで待ちますよね
また Linux だと最後に & をつけて Windows だと start コマンドを使うとバックグラウンド実行ができます
Windows で start コマンドを `` や exec などの通常の方法でコマンド実行すると start したコマンドはバックグラウンドで動いているのに 終わるまで php の処理が進まないです
Linux の & だとすぐに次のコマンドに進みますし Windows でもコマンドプロンプトで直接 start コマンドを実行すると すぐに次のコマンドが入力できるようになってます
ですが php から使ってると呼び出したコマンドが完了まで次に進めません
プロセスをオープンしてすぐにクローズします
これだと start したのがバックグラウンドで実行されて php の処理は次に進めることができます
勘違いしそうになりますが pclose があるから次に進めてるわけではないです
開いたらちゃんと閉じようということでクローズしてるだけで popen の時点ですぐ次の pclose に進んでいます
popen で待機状態になってたら pclose 意味ないですしね
pclose されるのは popen したコマンドが終わってからになるので pclose したら中断される?という心配は無用です
実は普通に echo とかするとバックグラウンドプロセスを起動した画面に表示されます
ただし 第二引数のモードを w にしておかないとダメです
popen は開いたプロセスと通信できるわけですが 通信は片方向だけです
それが読み取りなのか書き込みを選ぶのがモードです
r の読み取りを選ぶと 開いたプロセスの STDOUT (標準出力) が popen の返り値のファイルポインタ (プロセスですが php だとファイルポインタとして扱ってるみたい) になるようです
そのせいか echo しても直接画面には表示されません
popen の返り値のリソース型の値を使って取り出す必要があります
また pclose をしてしまうと アクセス方法がなくなります
ただなくなるじゃなくて echo などの標準出力への出力時にエラーが起きているようで それ以降の file_put_contents などは動作していませんでした
それと 上の方のサンプルにあったように 本来途切れないはずのところに文字が割り込んで表示されます
2 つのプロセスで var_dump のタイミングが重なるとすごいことになって ちゃんと読むことが難しそうなので そこは気をつけないとです
ウィンドウ出したくないなら
file_put_contents のファイルロックは動いてないっぽいです
結局なんで問題あった方のコードだと file_put_contents が終わるより先に is_readable が通るんだろう
ファイル書き込みは数文字だけで 遅くなるようなことしてないのに
ファイルを読み取ったら空になる
色々複雑ではあったのですが 関係ある部分だけ取り出すとこういうものです[プロセス1]
$filename = "sample";
while(true){
if(is_readable($filename)){
var_dump(file_get_contents($filename));
break;
}
}
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));
$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;
}
}
$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");
$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) ""
#
#*string(4) "#test"
#*string(0) ""
#
問題が起きたほうだと file_put_contents が開始してから終了するまでに readable のチェックが通ってるようです
3 パターンありますが バラバラです
var_dump の出力の途中に混ざってるのもあります
file_put_contents が終わる前に var_dump の出力があるときは 読み込むタイミングでは まだデータがないようで空文字です
再現してくれない テスト用の方
[テスト用]
##*readable
test
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 "#";
$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;
}
}
$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 "#";
$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
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 が通るんだろう
ファイル書き込みは数文字だけで 遅くなるようなことしてないのに
COMMENT
コメント一覧 (2)
-
- 2019/12/26 18:09
-
> file_put_contents のファイルロック
> 3 つ目の引数に FILE_EX をつけるだけでいいそうです
引数は「LOCK_EX」の間違いではないですか?
参考:
https://www.php.net/manual/ja/function.file-put-contents
-
- 2019/12/26 22:06
-
>>1
本当ですね
ありがとうございます
修正しました
試しに FILE_EX として動かしてみたら Notice が出る上に文字列とみなされて型が違うよって Warning まで出て動かなかったのでこのミスのせいでロックがおかしかったというわけではなくて 単に記事中のコードで書き間違えただけみたいです