flock — ファイルロックを取得する

PHP
  • カテゴリ: PHP
  • 掲載バージョン: PHP 8.4
  • 名前空間 / FQCN / コマンド: (グローバル関数)\flock
  • 関連: fopen / fclose / file_put_contents(LOCK_EX)/ ftruncate / rewind
  • 変更履歴: —

要点(TL;DR)

  • 複数プロセスから同一ファイルを安全に読む/書くためのアドバイザリロックを取得する
  • 最低限:flock($fp, LOCK_EX); // 書き込み用の排他ロック
  • 罠:
    • すべてのプロセスがflockを使って協調しないと効果がない(アドバイザリ)
    • ネットワークFS(NFSなど)では保証が弱い/無効な場合がある
    • ロックはハンドル単位fclose()やプロセス終了で解除される

概要

flock()はファイルハンドルに対して共有(読み取り)または排他(書き込み)ロックを取得します。ロックはアドバイザリであり、他プロセスも同様にflock()を使用してはじめて排他が成立します。ログの追記、カウンタ更新、同時書き込みの破損防止などで使います。

構文 / シグネチャ

bool flock(resource $stream, int $operation, int &$wouldBlock = null)

引数(表)

引数必須既定値説明
$streamresourcefopen()で開いたファイルハンドル
$operationintLOCK_SH(共有)/ LOCK_EX(排他)/ LOCK_UN(解除)に、必要ならLOCK_NB(非ブロッキング)をビットOR
$wouldBlockint&null非ブロッキング時に競合で失敗した場合、1がセットされる
  • 戻り値true(成功)/ false(失敗)
  • 例外/副作用:未対応FSや不正ハンドルでE_WARNINGLOCK_NBなしの競合はブロック。ロック状態はfclose()やプロセス終了で解除。

使用例

最小例(排他ロックで安全に書く)

<?php
$path = __DIR__ . '/sample.txt';
$fp = fopen($path, 'c+'); // ない場合は作成
if (!$fp) {
    throw new RuntimeException('ファイルを開けませんでした');
}

if (!flock($fp, LOCK_EX)) {
    fclose($fp);
    throw new RuntimeException('ロック取得に失敗しました');
}

// 内容を入れ替える例
ftruncate($fp, 0);
rewind($fp);
fwrite($fp, "hello\n");
fflush($fp);             // 明示的にフラッシュ(推奨)
flock($fp, LOCK_UN);     // 明示解除(fcloseでも解除される)
fclose($fp);

実務例(非ブロッキング+リトライでカウンタ更新)

<?php
function incrementCounter(string $path, float $timeoutSec = 3.0, int $sleepMs = 100): int
{
    $fp = fopen($path, 'c+');
    if (!$fp) {
        throw new RuntimeException('open failed: ' . $path);
    }

    $start = microtime(true);
    try {
        $would = 0;
        while (!flock($fp, LOCK_EX | LOCK_NB, $would)) {
            if ((microtime(true) - $start) >= $timeoutSec) {
                throw new RuntimeException('lock timeout');
            }
            usleep($sleepMs * 1000);
        }

        // クリティカルセクション
        $size = filesize($path);
        $size = ($size === 0 || $size === false) ? 0 : $size;
        rewind($fp);
        $current = $size ? (int)trim(fread($fp, $size)) : 0;
        $next = $current + 1;

        ftruncate($fp, 0);
        rewind($fp);
        fwrite($fp, (string)$next);
        fflush($fp);

        return $next;
    } finally {
        flock($fp, LOCK_UN);
        fclose($fp);
    }
}

// 使い方
// echo incrementCounter(__DIR__.'/counter.txt');

よくある落とし穴・注意

  • アドバイザリ:他プロセスもflockを使わないとすり抜ける。必ず両者で実装。
  • FS依存:NFS/一部のネットワーク共有、古い/特殊FSでは未対応や挙動不安定。Operation not supportedになることも。
  • モードとロックLOCK_SH読み取り可能に開く必要、LOCK_EXは書き込み可能に開く必要('r'/'r+'/'w'/'a'/'c'などを適切に選ぶ)。
  • ハンドル単位:同一プロセス内でも別ハンドルは別ロックfopenを複数回呼ぶと互いに競合する。
  • 性能:長時間の排他は詰まりの元。LOCK_NB+リトライ/タイムアウトを検討。
  • ファイル差し替え:ロックは同じinodeに対して有効。別ファイルへ原子的にrename()する戦略も有効。

代替・関連APIとの比較

  • file_put_contents(…, LOCK_EX):簡潔に一時的な排他で書き込み。細かな制御(共有ロック、長いクリティカルセクション、リトライ)は不可。
  • flock直使用:ロック期間・種類の制御、読み→更新→書き戻しなど複合操作に向く。
  • OS/DBロックsysvsem拡張(sem_acquire)やSQLiteのトランザクションなども選択肢。ファイル以外も含むならDB側に委譲。

テスト例(Pest)

<?php

it('rejects second non-blocking exclusive lock on the same file', function () {
    $file = tempnam(sys_get_temp_dir(), 't_');
    $fp1 = fopen($file, 'c+');
    $fp2 = fopen($file, 'c+');

    expect($fp1)->not->toBeFalse();
    expect($fp2)->not->toBeFalse();

    // 1つ目は取得できる
    expect(flock($fp1, LOCK_EX | LOCK_NB))->toBeTrue();

    // 2つ目は非ブロッキングだと失敗し、$wouldに1
    $would = 0;
    $ok = flock($fp2, LOCK_EX | LOCK_NB, $would);
    expect($ok)->toBeFalse();
    expect($would)->toBe(1);

    flock($fp1, LOCK_UN);
    fclose($fp1);
    fclose($fp2);
    unlink($file);
});

トラブルシュート(エラー別)

症状/エラー原因対処
flock(): Operation not supported対象FSがロック非対応/不完全(NFS/共有ドライブ等)ローカルFSへ移動、別FSに変更、DBロックへ設計変更
flock(): supplied resource is not a valid stream resource閉じた/不正なハンドルを渡したfopenの成否確認、fclose後に使わない
ロック取得が終わらない相手が長時間LOCK_EX保持/デッドロックLOCK_NB+タイムアウト・バックオフ・監視ログ
追記が壊れる/欠損ロックなしの同時書き込み、ftruncate/rewindの誤用追記はaモード+LOCK_EX、上書きはftruncate→rewind→write→fflush

参考リンク

長野県・北アルプス地方在住のフリーランスWebプログラマー。
「落ち着くためのWeb開発」をテーマに、訪れる人が安心して使えるサービスづくりを心がけています。

LaravelやWordPressなどのWebアプリケーション開発を得意とし、技術面の安定性はもちろん、運用後も長く活用できる設計を大切にしています。
静かな山間の暮らしから生まれる視点で、シンプルかつ本質的な解決策をご提案します。

野鳥観察も趣味のひとつで、特にミソサザイ(Wren)に魅力を感じています。
小さな体に反して力強く上向きの尾羽、そして澄んだ鳴き声が遠くまで響く姿に、静かな存在感と芯の強さを感じます。
このサイト名「Laravel Wren」には、そんなミソサザイのように、小さくても確かな価値を届けたいという想いを込めています。

信頼できるパートナーとして、そして気軽に相談できる存在として、あなたのWebプロジェクトをサポートします。

Yudai Tsuyuzakiをフォローする