RAII封装文件锁的核心思路是将锁的获取与释放严格绑定到对象生命周期:构造函数中申请锁(失败则抛异常),析构函数中无条件释放锁,确保异常安全与资源不泄漏。

RAII封装文件锁的核心思路
C++里没有标准库直接提供跨平台文件锁,但RAII的关键不是“有没有现成类”,而是“能否把锁的获取/释放绑定到对象生命周期”。本质是:构造函数申请锁(阻塞或失败),析构函数无条件释放锁。只要锁操作是可逆的、无副作用的,并且能保证析构一定发生(哪怕抛异常),就满足RAII前提。
-
flock()(Linux/macOS)和LockFileEx()(Windows)都支持可重入的独占锁,且释放锁不依赖调用上下文 - 必须避免在构造函数里抛异常后留下未释放的锁——所以
flock()建议配合O_NONBLOCK做非阻塞尝试,失败时直接抛异常,不遗留状态 - 不要用
std::fstream的打开模式模拟锁(如ios::noreplace),它不提供进程级互斥,仅是打开失败,不是真正的锁
一个最小可行的跨平台文件锁RAII类
实际写的时候别追求“一次写完所有平台”,先按目标系统实现,再用宏隔离。比如Linux下用flock()最轻量:
class FileLock {
int fd_;
public:
explicit FileLock(const char* path) : fd_(-1) {
fd_ = open(path, O_RDONLY);
if (fd_ == -1) throw std::runtime_error("open failed");
if (flock(fd_, LOCK_EX) == -1) {
close(fd_);
throw std::runtime_error("flock failed");
}
}
<pre class='brush:php;toolbar:false;'>~FileLock() {
if (fd_ != -1) {
flock(fd_, LOCK_UN); // 即使失败也继续,不能抛异常
close(fd_);
}
}
FileLock(const FileLock&) = delete;
FileLock& operator=(const FileLock&) = delete;};
- 构造函数里
open()必须用O_RDONLY或O_WRONLY,不能用O_RDWR——某些内核版本对O_RDWR加锁行为不一致 -
flock()作用于文件描述符,不是路径,所以open()不能省;多个进程打开同一路径会拿到不同fd,但flock()仍能协同工作 - 析构函数里
flock(fd_, LOCK_UN)失败(比如fd已被关闭)应静默忽略,RAII对象销毁阶段禁止抛异常
为什么不能用std::lock_guard套文件锁
std::lock_guard设计目标是配合std::mutex这类内存中同步原语,它假设lock()/unlock()是快速、无资源分配、无系统调用的。而文件锁:
立即学习“C++免费学习笔记(深入)”;
-
flock()可能阻塞(除非设LOCK_NB),违反std::lock_guard“构造即锁定”的隐含契约 - 锁的粒度是整个文件,不是某个内存地址,无法用
std::mutex语义建模 - 没有标准方式把
flock()封装成符合BasicLockable概念的对象(缺少try_lock()稳定实现)
所以硬套std::lock_guard<filelock></filelock>不仅没好处,反而掩盖了错误处理逻辑——你得自己检查flock()返回值,而不是依赖模板自动处理。
临时读写场景下的典型误用
多人同时写同一配置文件时,常见错误是“先读再锁再写”,这中间存在竞态窗口:
// ❌ 错误:读取和加锁之间有时间差
auto data = read_file("config.json"); // 可能读到旧内容
FileLock lock("config.json"); // 此刻别人可能已修改并写回
write_file("config.json", new_data);
正确做法是:
- 锁必须在任何访问文件前就持有,包括读
- 如果只是读,用
LOCK_SH;如果要写,必须用LOCK_EX,且读写不能并发 - 更安全的模式是:打开文件 → 加锁 →
read()或write()→ 解锁 → 关闭
临时锁定的本质不是“保护一段代码”,而是“保护对特定文件的一次原子性访问序列”。RAII只是让这个序列的边界更清晰,不是魔法。


















