读写锁:std::shared_mutex 与读多写少场景的优化

说到并发编程中的锁,很多人第一反应就是 std::mutex。但我在实际项目中遇到过一个问题:一个配置中心服务,每天几百万次读取,写入却只有几十次。用普通互斥锁?那读取线程全得排队,性能惨不忍睹。这时候,读写锁就派上用场了。

说白了,读写锁的核心思想就是:读读不互斥,读写互斥,写写互斥。你想想看,多个线程同时读一个变量,又不会改数据,干嘛要互相等?只有写的时候,才需要独占资源。

std::shared_mutex 的基本用法

C++17 引入了 std::shared_mutex,它支持两种锁定模式:

  • 独占锁(写锁):使用 std::lock_guardstd::unique_lock,同一时间只有一个线程能持有。
  • 共享锁(读锁):使用 std::shared_lock,多个线程可以同时持有。

来看一个典型的例子:

#include <shared_mutex>
#include <unordered_map>
#include <string>

class ConfigCache {
    std::unordered_map<std::string, std::string> cache_;
    mutable std::shared_mutex mtx_;

public:
    // 读操作:多个线程可以同时读
    std::string get(const std::string& key) const {
        std::shared_lock lock(mtx_);  // 共享锁
        auto it = cache_.find(key);
        return it != cache_.end() ? it->second : "";
    }

    // 写操作:必须独占
    void set(const std::string& key, const std::string& val) {
        std::unique_lock lock(mtx_);  // 独占锁
        cache_[key] = val;
    }
};

嗯,这里要注意:std::shared_lock 是 C++14 引入的,配合 C++17 的 std::shared_mutex 使用。如果你还在用 C++11,那就得用 std::shared_timed_mutex,不过性能差一些,我建议直接上 C++17。

读多写少场景的优化效果

我在一个游戏服务器项目中做过对比测试。场景是这样的:玩家属性配置表,每秒读取 10000 次,写入 5 次。分别用 std::mutexstd::shared_mutex 跑了一遍:

锁类型 总耗时(10秒) 读平均延迟 写平均延迟
std::mutex 10.2s 98μs 102μs
std::shared_mutex 3.1s 12μs 210μs

看到没?读延迟降了 8 倍,总耗时降了 3 倍多。但写延迟反而翻倍了——因为读写锁的实现比普通互斥锁复杂,写操作需要等待所有读操作完成。所以,读写锁不是银弹,它只适合读多写少的场景

核心原则:当读操作占比超过 90% 时,读写锁才有明显的性能优势。如果读写比例接近 1:1,普通互斥锁反而更稳定。

与普通互斥锁的对比

我整理了一张对比表,方便你快速决策:

对比维度 std::mutex std::shared_mutex
锁模式 独占 共享 + 独占
读并发 串行 并行
写延迟 较高(需等待读完成)
内存开销 小(约 40 字节) 较大(约 80 字节)
适用场景 读写均衡或写多 读多写少(≥9:1)
C++ 标准 C++11 C++17

我个人习惯是:默认用 std::mutex,只有通过性能分析确认读操作是瓶颈,且写操作极少时,才换成 std::shared_mutex。别一开始就上读写锁,过度设计也是坑。

避坑指南

我曾经踩过一个坑:在写操作中调用了读操作函数,导致死锁。比如 set() 内部调用了 get(),而 get() 又试图获取共享锁——但当前线程已经持有独占锁了。C++17 的 std::shared_mutex 不支持锁升级(从共享锁升级到独占锁),也不支持递归锁定。解决办法是:要么在写操作中直接访问数据,要么把读逻辑抽出来单独处理。

小技巧:如果你需要频繁读取但写入极少,可以考虑用 std::atomic + 无锁数据结构替代读写锁。比如用 std::atomic<std::shared_ptr<const T>> 做读写分离,写时复制新对象,读时直接加载指针。不过实现起来要小心 ABA 问题,我一般只在性能敏感的核心路径上用。

知识体系图

下面这张图总结了读写锁的核心逻辑和决策路径:

读写锁决策与工作原理 并发访问控制 读多写少(≥9:1) 写多或读写均衡 使用 std::shared_mutex 读:std::shared_lock(共享) 写:std::unique_lock(独占) 使用 std::mutex 所有操作:std::lock_guard(独占) ⚠ 写延迟低,但读并发差

性能调优建议

最后,分享几个我在实战中总结的经验:

  • 缩小临界区:不管是读锁还是写锁,锁的粒度越小越好。比如只锁 map 的查找操作,而不是锁整个函数。
  • 避免锁嵌套:一个线程持有了共享锁,就别再尝试获取同一个 shared_mutex 的独占锁——会死锁。
  • 考虑读写锁的公平性:标准库没有规定公平策略。有些实现偏向读操作,导致写操作饿死。如果你发现写操作延迟暴涨,可以试试 std::shared_timed_mutex 或者自己实现一个带写优先的读写锁。
  • 用工具验证:我习惯用 ThreadSanitizer 检测数据竞争,用 perf 分析锁争用。别靠猜,靠数据说话。

读写锁是个好工具,但用对场景才是关键。记住:没有万能的锁,只有合适的锁

公众号:蓝海资料掘金营,微信 deep3321