核心是三个状态变量始终由同一把互斥锁保护:
- activeReaders_:当前持有读锁的线程数。
- waitingWriters_:已经提出写锁申请、但尚未获得锁的线程数。
- writerActive_: 是否有线程持有写锁。
用一把互斥锁保护"谁有资格进入临界区"的决策过程,用两个条件变量管理等待队列,而不是用互斥锁保护整个读写过程。
当前实现满足常规互斥、读共享、写独占和 RAII 异常释放要求,但仍需注意以下使用边界:
- 不支持递归加锁,也不支持持有读锁期间升级为写锁。
- 解锁必须由持锁线程执行,不能跨线程转移守卫。
- 锁对象析构时必须保证没有持锁或等待中的线程。
- 写者持续到来时,读者可能饥饿,这是写者优先策略的固有取舍。
几个小问题:
1. 既然内部使用了 std::mutex ,多个读者是不是也被串行化了?
答案是:只有获取读锁的过程被短暂串行化,真正的读取过程仍然可以并发。read_lock() 返回前,局部变量 unique_lock 析构,释放 mtx_;但 activeReaders_ 已经增加,因此读锁的逻辑所有权仍然存在。互斥锁管理状态,计数器代表读锁所有权。
读者不仅需要检查有没有正在写的写者,还需要检查有没有等待的写者。所以读者能否进入的条件是:!writerActive_ && waitingWriters_ == 0; 而写者能否进入的条件是: **!writerActive_ && activeReaders_ == 0。**这是实现写者优先的关键。
2. read_unlock() 由最后一个读者负责唤醒写者
activeReaders_ == 0 时唤醒写者,为什么不让每个读者退出时都通知?
因为只要还有一个活跃读者,写者就不可能进入。通知它也没有意义。所以只有最后一个读者退出,才会使写锁的进入条件从不满足变成可能满足。
3. write_lock() 为什么先增加 waitingWriters_ 再等待?
因为写者即使暂时无法进入,也要立即阻止新的读者加入。否则可能出现: 旧的读者不断退出,新的读者不断进入,于是活跃读者数始终无法降到零,写者长期饥饿。
4. write_unlock() 使用两种通知方式
- notify_one():一次只能有一个写者,因此只唤醒一个。
- notify_all():可以允许多个读者同时进入,因此唤醒所有等待读者。
通知并不等于立即获得读写锁。被唤醒的线程仍然需要重新获取 mtx_,并检查进入条件。所以 notify_all() 也不能保证所有读者都能进入:如果另一个写者先登记,读者还需要继续等待。
5. 为什么 condition_variable 不会漏唤醒?
假设线程刚判断需要等待,但还没有真正睡眠,另一个线程就调用 notify,会不会永远睡下去?
cv.wait(lk, predicate);
这里的 wait(lk) 包含三个关键动作:
- 原子地释放关联互斥锁并进入等待。
- 等待通知,且允许虚假唤醒。
- 被唤醒后重新获取互斥锁。
因为谓词检查和状态修改都受到同一把 mtx_ 保护,所以通知方无法在等待方检查完条件、但尚未完成释放互斥锁并进入等待的交接过程中,偷偷完成状态修改。
条件变量本身不保存事件;真正保存状态的是 activeReaders_、waitingWriters_ 和 writerActive_。
6. 这个实现的收益和代价
| 特点 | 收益 | 代价 |
|---|---|---|
| 单一互斥锁保护状态 | 容易保证状态一致性 | 获取和释放读写锁需要竞争 mutex |
| 写者优先 | 防止不断到来的读者饿死写者 | 读者可能饥饿 |
| 双条件变量 | 分别唤醒有资格进入的读者和写者 | 比单条件变量多一点管理复杂度 |
| 不在业务临界区持有 mtx_ | 允许多个读者真正并发 | 需要自行维护逻辑持锁状态 |
| RAII Guard | 发生异常时自动释放读写锁 | 必须遵守守卫对象的生命周期 |
| 不使用原子变量 | 无需手工推导复杂内存序 | 高频极短读操作可能产生 mutex 开销 |
代码如下。
cpp
#pragma once
#include <mutex>
#include <condition_variable>
#include <cstddef>
#include <cassert>
class RWLock {
public:
RWLock() = default;
~RWLock() = default;
RWLock(const RWLock&) = delete;
RWLock& operator=(const RWLock&) = delete;
// 读锁:无写者正在写,且无等待写者才能进入
void read_lock() {
std::unique_lock<std::mutex> lk(mtx_);
cvReaders_.wait(lk, [this] {
return !writerActive_ && waitingWriters_ == 0;
});
++activeReaders_;
}
void read_unlock() noexcept {
std::lock_guard<std::mutex> lk(mtx_);
assert(activeReaders_ > 0);
--activeReaders_;
// 最后一个读者退出,允许等待写者进入
if (activeReaders_ == 0 && waitingWriters_ > 0) {
cvWriters_.notify_one();
}
}
// 写锁:写者优先,等待所有现有读者退出
void write_lock() {
std::unique_lock<std::mutex> lk(mtx_);
++waitingWriters_;
try {
cvWriters_.wait(lk, [this] {
return !writerActive_ && activeReaders_ == 0;
});
} catch (...) {
// 等待异常时恢复写者计数
--waitingWriters_;
// 没有待写者时,重新放行读者
if (!writerActive_ && waitingWriters_ == 0) {
cvReaders_.notify_all();
}
throw;
}
--waitingWriters_;
writerActive_ = true;
}
void write_unlock() noexcept {
std::lock_guard<std::mutex> lk(mtx_);
assert(writerActive_);
writerActive_ = false;
if (waitingWriters_ > 0) {
// 写者优先
cvWriters_.notify_one();
} else {
// 无等待写者,放行所有读者
cvReaders_.notify_all();
}
}
// RAII 读锁守卫
class SharedGuard {
public:
explicit SharedGuard(RWLock& lock)
: lock_(lock) {
lock_.read_lock();
}
~SharedGuard() noexcept {
lock_.read_unlock();
}
SharedGuard(const SharedGuard&) = delete;
SharedGuard& operator=(const SharedGuard&) = delete;
private:
RWLock& lock_;
};
// RAII 写锁守卫
class ExclusiveGuard {
public:
explicit ExclusiveGuard(RWLock& lock)
: lock_(lock) {
lock_.write_lock();
}
~ExclusiveGuard() noexcept {
lock_.write_unlock();
}
ExclusiveGuard(const ExclusiveGuard&) = delete;
ExclusiveGuard& operator=(const ExclusiveGuard&) = delete;
private:
RWLock& lock_;
};
private:
std::mutex mtx_;
std::condition_variable cvReaders_;
std::condition_variable cvWriters_;
std::size_t activeReaders_ = 0;
std::size_t waitingWriters_ = 0;
bool writerActive_ = false;
};
使用方式例如:
cpp
RWLock rwlock;
int sharedData = 0;
void readData() {
RWLock::SharedGuard guard(rwlock);
int value = sharedData;
// 使用 value
}
void writeData() {
RWLock::ExclusiveGuard guard(rwlock);
++sharedData;
}