C++ 用互斥锁实现读写锁

核心是三个状态变量始终由同一把互斥锁保护:

  • 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) 包含三个关键动作:

  1. 原子地释放关联互斥锁并进入等待。
  2. 等待通知,且允许虚假唤醒。
  3. 被唤醒后重新获取互斥锁。

因为谓词检查和状态修改都受到同一把 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;
}
相关推荐
你怎么知道我是队长1 小时前
C++零基础入门第2周:数据类型、运算符与表达式完全指南(附20段可运行代码+避坑清单)
c++
say_fall2 小时前
【Linux系统编程】Ext 文件系统:磁盘与存储、文件与目录、现代计算机究竟如何寻找文件
linux·运维·服务器·数据库·c++
码流子2 小时前
移动收费怎么做?手持终端+App+收费中心的架构实践
java·开发语言·架构
雪落漂泊2 小时前
C++算法 滑动窗口篇(下)
开发语言·c++
云创智城-yuncitys2 小时前
YunParking 4.0:JDK 17 与 Spring Boot 3.2 的停车云升级
java·开发语言·spring boot
郭小李2 小时前
openFeign入门
java·开发语言·前端
wljt3 小时前
公司二-面试题
java·开发语言·mybatis
云和数据.ChenGuang4 小时前
Python 闭包详解:从原理到实战
linux·开发语言·人工智能·python·kubernetes
揣一尾游鱼yu4 小时前
内部类和Object笔记
java·开发语言·object·内部类