【PHP8.x】flock関数の使い方

flock関数の使い方について、初心者にもわかりやすく解説します。

作成日: 更新日:

基本的な使い方

flock関数は、開いているファイルに対してロックの取得や解放を実行する関数です。Webアプリケーションなど、複数のプロセスが同時に同じファイルへアクセスする環境では、ファイルへの書き込み処理が衝突し、データが破損してしまう可能性があります。flock関数は、このような競合状態を防ぎ、ファイルの整合性を保つために使用される排他制御の仕組みです。第一引数にはfopen関数などで取得したファイルポインタを、第二引数にはロックの種類を指定する定数を渡します。例えば、他のプロセスからの読み込みは許可するが書き込みは防ぐ「共有ロック(LOCK_SH)」や、自分以外の全てのプロセスからの読み書きをブロックする「排他ロック(LOCK_EX)」などがあります。ファイルへの処理が完了した後は、必ず「ロック解放(LOCK_UN)」を指定してロックを解除する必要があります。このロックはアドバイザリロックと呼ばれる協調的な仕組みであり、flock関数を使用するプロセス間でのみ有効です。ロックの取得や解放に成功した場合はtrueを、失敗した場合はfalseを返します。

構文(syntax)

1flock(resource $file, int $operation): bool

引数(parameters)

resource $stream, int $operation, ?int &$would_block = null

  • resource $stream: ロックをかけるファイルリソースを指定します。fopen() などで取得したファイルポインタを指定します。
  • int $operation: 実行するロック操作を指定します。LOCK_SH(共有ロック)、LOCK_EX(排他ロック)、LOCK_UN(ロック解除)などの定数を使用します。
  • ?int &$would_block = null: ロックがすぐに取得できなかった場合に、その旨を示す整数値が格納されます。true の場合はロックがブロックされたことを示します。

戻り値(return)

bool

ファイルロック操作の成功・失敗を示す真偽値(boolean)が返されます。ロック操作が成功した場合は true、失敗した場合は false が返されます。

サンプルコード

PHP flockによるタイムアウト付きファイルロック取得

1<?php
2
3/**
4 * ファイルに対してタイムアウト付きで排他ロックを取得し、処理を実行します。
5 *
6 * @param string $filePath ロックするファイルのパス
7 * @param int $timeoutSeconds ロック取得を試みる最大時間(秒)
8 * @param int $pollIntervalMicroseconds ロック状態をポーリングする間隔(マイクロ秒)
9 * @return bool ロック取得と処理の実行が成功した場合は true、それ以外は false
10 */
11function acquireFileLockWithTimeout(
12    string $filePath,
13    int $timeoutSeconds = 10,
14    int $pollIntervalMicroseconds = 500000 // 0.5秒
15): bool {
16    $fp = null;
17    $locked = false; // ロックが取得できたかどうかのフラグ
18
19    try {
20        // ファイルを開く (読み書きモード、ファイルが存在しない場合は作成)
21        // 'c+' はファイルが存在しない場合は作成し、読み書きモードで開きます。
22        // ファイルポインタはファイルの先頭に置かれます。
23        $fp = fopen($filePath, 'c+');
24        if ($fp === false) {
25            echo "エラー: ファイルを開けませんでした: {$filePath}\n";
26            return false;
27        }
28
29        $startTime = time();
30        $wouldBlock = null; // flockがブロックされた場合に設定される変数 (PHP 8で導入)
31
32        // 指定されたタイムアウト時間内でロック取得を試行
33        while ((time() - $startTime) < $timeoutSeconds) {
34            // flock($fp, LOCK_EX | LOCK_NB, $wouldBlock)
35            // LOCK_EX: 排他ロック(他のプロセスはファイルの読み書きができません)
36            // LOCK_NB: 非ブロッキングモード(ロックが取得できない場合でも、すぐにfalseを返します)
37            // $wouldBlock: LOCK_NB指定時にロック取得に失敗した場合、true/1が設定されます。
38            if (flock($fp, LOCK_EX | LOCK_NB, $wouldBlock)) {
39                $locked = true;
40                echo "ファイルロックを取得しました。\n";
41                break; // ロック成功、ループを抜ける
42            } elseif ($wouldBlock) {
43                // ロックが他のプロセスによってブロックされている場合
44                // 少し待機してから再試行します。
45                usleep($pollIntervalMicroseconds);
46            } else {
47                // flockが予期せず失敗した場合($wouldBlockが設定されない、またはfalseの場合)
48                echo "エラー: flock関数が予期せず失敗しました。\n";
49                return false;
50            }
51        }
52
53        if (!$locked) {
54            echo "エラー: ファイルロックの取得に失敗しました (タイムアウト: {$timeoutSeconds}秒)。\n";
55            return false;
56        }
57
58        // --- ここにロック中に実行したい処理を記述します ---
59        // 例として、ファイルにタイムスタンプを追記します。
60        echo "ロック中に処理を実行します... (2秒間待機)\n";
61        sleep(2); // 処理を模倣するために2秒間一時停止
62        
63        // ファイルの末尾に内容を追記
64        file_put_contents($filePath, "Locked at " . date('Y-m-d H:i:s') . "\n", FILE_APPEND);
65        echo "処理が完了しました。\n";
66        // --------------------------------------------------
67
68        return true; // 処理成功
69
70    } catch (Throwable $e) { // PHP 7からExceptionだけでなくErrorもキャッチできるようThrowableを使用
71        echo "致命的なエラーが発生しました: " . $e->getMessage() . "\n";
72        return false;
73    } finally {
74        // ファイルハンドルが開いている場合、ロックを解除しファイルを閉じます
75        if ($fp !== null) {
76            if ($locked) {
77                flock($fp, LOCK_UN); // LOCK_UN: ロック解除
78                echo "ファイルロックを解除しました。\n";
79            }
80            fclose($fp);
81        }
82    }
83}
84
85// --- サンプルコードの実行例 ---
86// このコードを複数同時に実行して、ロックが機能するかテストできます。
87$fileToLock = 'my_application.lock';
88$timeout = 5; // ロック取得のタイムアウトを5秒に設定
89
90echo "ファイルロック取得処理を開始します...\n";
91if (acquireFileLockWithTimeout($fileToLock, $timeout)) {
92    echo "ファイルロック処理が正常に完了しました。\n";
93} else {
94    echo "ファイルロック処理が失敗しました。\n";
95}
96
97?>

PHPのflock関数は、複数のプログラムが同時にファイルへアクセスする際に、データの破損を防ぎ、整合性を保つための「ファイルロック」機能を提供します。この関数は、ファイルリソース、実行するロック操作の種類(排他ロック、共有ロック、ロック解除など)、そしてPHP 8で導入された、ロックがブロックされた場合にその状態を示す変数$would_blockを引数に指定します。ロック操作が成功したか否かは、戻り値の真偽値で判断できます。

サンプルコードは、flock関数を用いて、指定されたタイムアウト時間内にファイルロックを取得し、その間に処理を実行する仕組みを示しています。LOCK_EX(排他ロック)とLOCK_NB(非ブロッキングモード)を組み合わせて使用し、ロックがすぐに取得できない場合でも処理を停止させずに$would_block引数からロックの競合状態を判断します。ロックがブロックされている間は短い時間待機(usleep)し、タイムアウトになるまでロック取得を繰り返し試みます。これにより、ファイルへの排他的アクセスを一定時間内で安全に確保し、処理完了後にロックを解除する堅牢なファイル操作が実現されます。これは、複数のプログラムが共有リソースを安全に扱うための重要な技術です。

flock関数は、指定されたファイル全体をロックします。NFSなどのネットワークファイルシステム上では、期待通りに動作しない場合があるため注意が必要です。

サンプルコードでは、LOCK_NB(非ブロッキングモード)を指定することで、ロックが取得できない場合でも処理をブロックせず、すぐに結果を返します。PHP 8以降では、$would_block引数を通じて、ロックが他のプロセスによってブロックされたかどうかを確認できるため、タイムアウト処理を実装する際に役立ちます。

ロック取得に失敗した場合にusleepで短い間隔を置いて再試行することは、CPU負荷を抑えつつ、ロックが解放されるのを待つために重要です。

finallyブロックは、ロックの成否にかかわらず、確実にファイルロックを解除し、ファイルハンドルを閉じるために必須です。これにより、リソースの解放漏れやデッドロックといった問題を未然に防ぎ、アプリケーションの安定性を高めます。

PHP flock でファイルロックを安全に取得する

1<?php
2
3/**
4 * ファイルロックを使用して安全にファイルに書き込む処理を管理します。
5 * flockはファイル記述子に対して動作するため、プロセス間ロックに使用されます。
6 *
7 * @param string $filePath ロックするファイルのパス
8 * @param string $content ファイルに書き込む内容
9 */
10function manageFileLock(string $filePath, string $content): void
11{
12    // ファイルを開く。'c+'モードは、ファイルが存在しない場合は作成し、読み書きを許可します。
13    // ファイルポインタはファイルの先頭に置かれます。
14    $fileHandle = fopen($filePath, 'c+');
15
16    if ($fileHandle === false) {
17        echo "エラー: ファイル '{$filePath}' を開けませんでした。\n";
18        return;
19    }
20
21    echo "ブロックする排他ロックを取得しようとしています...\n";
22
23    // 排他ロック (LOCK_EX) をブロックモードで取得します。
24    // 他のプロセスがロックを保持している場合、ここでスクリプトは待機します。
25    if (flock($fileHandle, LOCK_EX)) {
26        echo "ブロックする排他ロックを正常に取得しました。\n";
27
28        // ロックが保持されている間に他のプロセスがアクセスしようとする状況をシミュレートするため、
29        // 2秒間待機します。
30        echo "ロックを2秒間保持中 (処理シミュレーション)... \n";
31        sleep(2);
32
33        // ファイルの内容をクリアし、ポインタを先頭に戻してから新しい内容を書き込みます。
34        ftruncate($fileHandle, 0); // ファイルを0バイトに切り詰める
35        rewind($fileHandle);       // ファイルポインタを先頭に戻す
36        fwrite($fileHandle, $content);
37        fflush($fileHandle);       // バッファされた出力を強制的にファイルに書き込む
38
39        echo "ファイルに書き込みました。\n";
40
41        // 同じファイルに対して、別のファイルハンドルを使って非ブロックロックを試みます。
42        // これは、最初のロックがまだ保持されているため、失敗すると予想されます。
43        echo "\n同じファイルに対し、別のハンドルで非ブロックロックを試みます...\n";
44        $secondFileHandle = fopen($filePath, 'c+');
45        if ($secondFileHandle === false) {
46            echo "エラー: 2番目のファイルハンドルを開けませんでした。\n";
47        } else {
48            $wouldBlock = null;
49            // 排他ロック (LOCK_EX) を非ブロックモード (LOCK_NB) で取得しようとします。
50            if (flock($secondFileHandle, LOCK_EX | LOCK_NB, $wouldBlock)) {
51                // ここに到達した場合、予想外にロックが取得されたことを示します。
52                // (例: 最初のロックがすでに解放されていた、またはファイルシステム固有の動作)
53                echo "警告: 2番目のハンドルで非ブロックロックを予期せず取得しました。\n";
54                flock($secondFileHandle, LOCK_UN); // すぐに解放します
55            } else {
56                // ロック取得に失敗した場合の処理
57                if ($wouldBlock) {
58                    // $wouldBlockがtrueの場合、他のプロセスがロックを保持しているため、
59                    // 非ブロック操作が失敗したことを意味します。これが「flockが効かない」
60                    // と感じられる主なケースの1つですが、実際には「意図通りブロックされた」
61                    // と解釈すべきです。
62                    echo "2番目のハンドルでの非ブロックロックは、ブロックされることが検出されました (期待通り)。\n";
63                    echo "これがLOCK_NBで「flockが効かない」場合に対処する方法です: ロック済みと報告されます。\n";
64                } else {
65                    // $wouldBlockがfalseの場合、一般的なflockのエラーです (例: ファイルシステムがロックをサポートしていない)。
66                    echo "エラー: 2番目のハンドルでの非ブロックロックは、不明な理由で失敗しました。\n";
67                }
68            }
69            fclose($secondFileHandle); // 2番目のファイルハンドルを閉じます
70        }
71
72        // 最初のロックを解放します。
73        flock($fileHandle, LOCK_UN);
74        echo "\n最初のロックを解放しました。\n";
75    } else {
76        // ブロックするロックの取得に失敗した場合。これは通常、ファイルシステムの問題や権限の問題です。
77        // 他のプロセスがロックを保持している場合、flockは待機するため、ここには到達しません。
78        echo "エラー: ファイル '{$filePath}' のブロックする排他ロックを取得できませんでした。\n";
79    }
80
81    // 最初のファイルハンドルを閉じます。
82    fclose($fileHandle);
83}
84
85// ロックファイルとして使用するパスを定義
86$lockFilePath = __DIR__ . '/app_data.lock';
87$dataContent = "アプリケーションデータ最終更新: " . date('Y-m-d H:i:s') . "\n";
88
89// ロック管理ロジックを実行
90manageFileLock($lockFilePath, $dataContent);
91
92// オプション: サンプル実行後にロックファイルを削除する場合 (開発/テスト用途)
93// unlink($lockFilePath);

PHPのflock関数は、複数のPHPプロセスが同じファイルへ同時に書き込みを行う際に、データの破損を防ぎ、整合性を保つための「ファイルロック」機能を提供します。これは、ファイルを開いた際に得られるリソース $stream に対して動作し、どのようにロックするかを $operation 引数で指定します。$operationには、他のプロセスからの書き込みを禁止する「排他ロック (LOCK_EX)」や、複数プロセスでの読み込みを許可する「共有ロック (LOCK_SH)」、そしてロックを解放する「ロック解除 (LOCK_UN)」などがあります。

サンプルコードでは、まずファイルをc+モードで開いた後、flock($fileHandle, LOCK_EX)で排他ロックをブロックモードで取得しています。これにより、他のプロセスがロックを保持している場合は、このスクリプトはロックが解放されるまで待機します。ロック取得後は、ファイルに安全に書き込みを行い、処理が完了したらflock($fileHandle, LOCK_UN)でロックを解放します。

特に注目すべきは、flock($secondFileHandle, LOCK_EX | LOCK_NB, $would_block)の部分です。ここでは、非ブロックモード (LOCK_NB) で排他ロックの取得を試みています。非ブロックモードは、ロック取得に失敗しても待機せず、すぐに結果を返します。この時、もしロック取得に失敗した原因が「他のプロセスがロックを保持しているため」であれば、オプション引数 $would_blocktrueがセットされます。初心者が「flockが効かない」と感じるケースの多くは、このLOCK_NBが既存のロックによってブロックされ、意図通りロックが取得できなかった状況を指します。$would_blockを確認することで、その原因を正確に把握し、対処することができます。flockはロックの取得・解放が成功した場合はtrue、失敗した場合はfalseを戻り値として返します。

flockはファイル記述子に対して動作し、主に複数のプロセス間での排他制御に利用されます。LOCK_NBを指定した際にロックが取得できない場合、返り値がfalseとなり、第三引数の$would_blockで他のプロセスがロックを保持しているか確認できます。これは「flockが効かない」のではなく、意図通りブロックされている状態ですので注意が必要です。NFSなどのネットワークファイルシステムでは、flockが期待通り機能しない場合があるため、動作保証が難しいことがあります。ロックを取得した後は、必ずflock(LOCK_UN)で解放するか、ファイルハンドルを閉じるようにしましょう。解放し忘れは他のプロセスのデッドロックの原因となります。

関連コンテンツ

【PHP8.x】flock関数の使い方 | いっしー@Webエンジニア