PHP flock()函数

flock()函数用于对文件进行锁定操作,可以避免多个进程同时访问同一个文件时产生的冲突。

语法

bool flock ( resource $handle , int $operation [, int &$wouldblock ] )

参数说明

参数 描述
handle 文件系统指针,通常是使用fopen()创建的资源
operation 锁定类型,可以是以下常量之一:
  • LOCK_SH - 共享锁(读取锁)
  • LOCK_EX - 独占锁(写入锁)
  • LOCK_UN - 释放锁
  • LOCK_NB - 非阻塞模式(与上述锁定类型组合使用)
$wouldblock 可选参数。如果设置为1,表示当锁不可用时,flock()会阻塞(默认行为)

锁定类型常量详解

常量 描述 应用场景
LOCK_SH 1 共享锁(读取锁) 多个进程可以同时获取共享锁进行读取操作
LOCK_EX 2 独占锁(写入锁) 只有一个进程可以获取独占锁进行写入操作
LOCK_UN 3 释放锁 释放当前进程持有的锁
LOCK_NB 4 非阻塞模式 与LOCK_SH或LOCK_EX组合使用,避免阻塞等待

示例代码

示例1:基本的文件锁定与写入

<?php
// 以读写方式打开文件(如果不存在则创建)
$fp = fopen("counter.txt", "c+");

if (flock($fp, LOCK_EX)) {  // 获取独占锁(写入锁)
    // 读取当前计数
    $count = (int)fgets($fp);

    // 增加计数
    $count++;

    // 回到文件开头
    fseek($fp, 0);

    // 写入新的计数值
    fwrite($fp, $count);

    // 截断文件(确保不会有多余内容)
    ftruncate($fp, ftell($fp));

    // 释放锁
    flock($fp, LOCK_UN);

    echo "当前计数: " . $count;
} else {
    echo "无法获取文件锁";
}

// 关闭文件
fclose($fp);
?>

示例2:非阻塞锁定

<?php
$fp = fopen("data.txt", "c+");

// 尝试以非阻塞方式获取独占锁
if (flock($fp, LOCK_EX | LOCK_NB)) {
    echo "成功获取锁,正在写入数据...<br>";

    // 模拟长时间操作
    sleep(2);
    fwrite($fp, "Data written at: " . date('Y-m-d H:i:s'));

    flock($fp, LOCK_UN);
    echo "写入完成,已释放锁";
} else {
    echo "无法立即获取锁,文件可能正被其他进程使用";
    echo "<br>你可以选择:";
    echo "<br>1. 等待重试";
    echo "<br>2. 执行其他操作";
    echo "<br>3. 给用户显示繁忙信息";
}

fclose($fp);
?>

示例3:共享锁(多进程同时读取)

<?php
// 模拟多个进程同时读取文件
$fp = fopen("readonly_data.txt", "r");

if (flock($fp, LOCK_SH)) {  // 获取共享锁
    echo "进程 " . getmypid() . " 获取了共享锁<br>";

    // 多个进程可以同时持有共享锁
    $content = fread($fp, filesize("readonly_data.txt"));
    echo "读取到的内容长度: " . strlen($content) . " 字节<br>";

    // 模拟读取操作耗时
    sleep(1);

    flock($fp, LOCK_UN);
    echo "进程 " . getmypid() . " 已释放锁";
} else {
    echo "无法获取共享锁";
}

fclose($fp);
?>

示例4:锁定的实用类封装

<?php
class FileLocker {
    private $fp;
    private $filename;

    public function __construct($filename) {
        $this->filename = $filename;
        $this->fp = fopen($filename, "c+");

        if (!$this->fp) {
            throw new Exception("无法打开文件: " . $filename);
        }
    }

    /**
     * 获取独占锁
     */
    public function lockExclusive($blocking = true) {
        $operation = LOCK_EX;
        if (!$blocking) {
            $operation |= LOCK_NB;
        }

        return flock($this->fp, $operation);
    }

    /**
     * 获取共享锁
     */
    public function lockShared($blocking = true) {
        $operation = LOCK_SH;
        if (!$blocking) {
            $operation |= LOCK_NB;
        }

        return flock($this->fp, $operation);
    }

    /**
     * 释放锁
     */
    public function unlock() {
        return flock($this->fp, LOCK_UN);
    }

    /**
     * 读取文件内容(带锁保护)
     */
    public function readWithLock() {
        if ($this->lockShared()) {
            fseek($this->fp, 0);
            $content = stream_get_contents($this->fp);
            $this->unlock();
            return $content;
        }
        return false;
    }

    /**
     * 写入文件内容(带锁保护)
     */
    public function writeWithLock($content) {
        if ($this->lockExclusive()) {
            fseek($this->fp, 0);
            fwrite($this->fp, $content);
            ftruncate($this->fp, ftell($this->fp));
            $this->unlock();
            return true;
        }
        return false;
    }

    public function __destruct() {
        if ($this->fp) {
            fclose($this->fp);
        }
    }
}

// 使用示例
try {
    $locker = new FileLocker("data.txt");

    // 读取数据
    $content = $locker->readWithLock();
    echo "读取到的内容: " . htmlspecialchars($content) . "<br>";

    // 写入数据
    $newContent = "更新后的数据: " . date('Y-m-d H:i:s');
    if ($locker->writeWithLock($newContent)) {
        echo "数据写入成功";
    } else {
        echo "数据写入失败";
    }
} catch (Exception $e) {
    echo "错误: " . $e->getMessage();
}
?>

注意事项

重要提示:
  • 锁是基于进程的:flock()锁是进程级别的,同一个进程内的多个线程可以同时操作已锁定的文件
  • 锁与文件句柄关联:锁与文件句柄(resource)关联,而不是与文件关联。关闭文件句柄或脚本结束时会自动释放锁
  • 锁的继承:通过fork()创建的子进程不会继承父进程的文件锁
  • NFS文件系统:在NFS或其他网络文件系统上,flock()可能无法正常工作
  • Windows系统差异:在Windows上,flock()使用强制性锁,而在Unix-like系统上通常使用建议性锁
  • 锁的粒度:flock()锁定整个文件,无法锁定文件的特定部分
  • 死锁风险:不当的锁定顺序可能导致死锁,特别是在多个文件需要锁定的时候

示例5:避免死锁的锁定策略

<?php
// 锁定多个文件时的死锁避免策略
function safeMultiFileWrite($files, $data) {
    $handles = [];
    $locked = [];

    try {
        // 第一阶段:打开所有文件
        foreach ($files as $filename) {
            $fp = fopen($filename, "c+");
            if (!$fp) {
                throw new Exception("无法打开文件: $filename");
            }
            $handles[$filename] = $fp;
        }

        // 第二阶段:按照固定顺序获取锁(避免死锁的关键)
        $sortedFiles = array_keys($handles);
        sort($sortedFiles);  // 按字母顺序排序,确保所有进程使用相同的锁定顺序

        foreach ($sortedFiles as $filename) {
            $fp = $handles[$filename];

            // 使用非阻塞模式尝试获取锁
            if (!flock($fp, LOCK_EX | LOCK_NB)) {
                // 如果获取失败,释放所有已获取的锁
                foreach ($locked as $lockedFile) {
                    flock($handles[$lockedFile], LOCK_UN);
                }
                throw new Exception("无法获取文件 $filename 的锁,可能发生死锁风险");
            }

            $locked[] = $filename;
        }

        // 第三阶段:所有锁都已获取,执行操作
        foreach ($handles as $filename => $fp) {
            fseek($fp, 0);
            fwrite($fp, $data[$filename]);
            ftruncate($fp, ftell($fp));
        }

        // 第四阶段:释放所有锁
        foreach ($locked as $filename) {
            flock($handles[$filename], LOCK_UN);
        }

        return true;

    } catch (Exception $e) {
        // 清理:关闭所有文件句柄
        foreach ($handles as $fp) {
            fclose($fp);
        }
        return false;
    }
}

// 使用示例
$files = ["file1.txt", "file2.txt", "file3.txt"];
$data = [
    "file1.txt" => "Data for file1",
    "file2.txt" => "Data for file2",
    "file3.txt" => "Data for file3"
];

if (safeMultiFileWrite($files, $data)) {
    echo "所有文件写入成功";
} else {
    echo "文件写入失败";
}
?>

相关函数

  • fopen() - 打开文件或URL
  • fclose() - 关闭一个已打开的文件指针
  • ftruncate() - 将文件截断到给定的长度
  • fseek() - 在文件指针中定位
  • sem_acquire() - 获取信号量(System V信号量)
  • shmop_open() - 创建或打开共享内存块

典型应用场景

  1. 访问计数器:多个用户同时访问时的计数器递增
  2. 日志文件写入:多进程同时写入日志文件
  3. 配置文件更新:防止配置文件在读取时被修改
  4. 缓存文件生成:防止多个进程同时生成相同的缓存文件
  5. 临时文件操作:确保临时文件操作的原子性