- カテゴリ: 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)
引数(表)
| 引数 | 型 | 必須 | 既定値 | 説明 | 
|---|---|---|---|---|
| $stream | resource | ✓ | — | fopen()で開いたファイルハンドル | 
| $operation | int | ✓ | — | LOCK_SH(共有)/ LOCK_EX(排他)/ LOCK_UN(解除)に、必要ならLOCK_NB(非ブロッキング)をビットOR | 
| $wouldBlock | int& | null | 非ブロッキング時に競合で失敗した場合、1がセットされる | 
- 戻り値:
true(成功)/false(失敗) - 例外/副作用:未対応FSや不正ハンドルでE_WARNING。
LOCK_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 | 

  
  
  
  