使用 std::shared_mutex 保护 lru 缓存可提升读多写少场景吞吐,get() 用 shared_lock 仅读查,未命中时释放后以 unique_lock 写入;须用 splice() 维持 list 迭代器有效,并先取尾节点 key 再删哈希表与 list。

用 std::shared_mutex 保护读多写少的 LRU 缓存
LRU 缓存天然适合读多写少场景,std::shared_mutex(C++17 起)能让你的 get() 并发执行、put() 独占操作,比全用 std::mutex 吞吐高得多。别用 std::shared_timed_mutex——它在多数标准库实现里性能更差,且已被 C++20 标记为 deprecated。
常见错误是把整个 get() 逻辑包进 shared_lock:比如查不到就去构造值再写入——这会触发写操作,但 shared_lock 不允许写,结果要么死锁,要么编译失败。正确做法是:只用 shared_lock 做“查+返回”,写操作必须升为 unique_lock。
-
get()开头用std::shared_lock<:shared_mutex></:shared_mutex>,仅读m_cache和m_lru_order - 查不到时立刻释放 shared_lock,再用
std::unique_lock<:shared_mutex></:shared_mutex>重试并写入 - 避免在锁内调用可能阻塞或抛异常的用户代码(如网络 IO、构造耗时对象)
list 迭代器失效?别直接存 std::list::iterator 到哈希表
LRU 的核心是快速移动节点到头部,常用 std::list 存键值对,哈希表(std::unordered_map)存键 → 迭代器映射。但 std::list 迭代器在 list 本身不被销毁的前提下是稳定的——这点没错,可一旦你用 splice() 把节点从中间移到头部,迭代器依然有效;但若用 erase() + push_front(),旧迭代器就失效了。
所以必须用 splice(),而不是删再插:
立即学习“C++免费学习笔记(深入)”;
auto it = m_lru_order.begin(); m_lru_order.splice(m_lru_order.begin(), m_lru_order, it); // ✅ 安全,it 仍有效
- 哈希表 value 类型应为
std::list<node>::iterator</node>,不是指针或索引 - 每次
get()后必须splice()到 front,不能只改哈希表里的值 - 注意
splice()第一个参数是目标位置,第二个是源 list,第三个是源迭代器——顺序错会导致静默错误
容量超限时,erase() 必须和 pop_back() 配合使用
缓存满时要踢掉最久未用项(m_lru_order.back()),但只删 list 不够:你还得从哈希表里同步删除对应键。漏删会导致哈希表持续膨胀,甚至后续 get() 查到已失效的迭代器,解引用崩溃。
典型错误是先 pop_back() 再从哈希表里找 key 删除——但 list 节点已丢,你根本拿不到它的 key。正确顺序是:从 list 尾部取节点 → 提取 key → 删哈希表 → 再删 list 节点。
- 不要依赖
list.back().key—— 如果Node是std::pair<k v></k>,那没问题;但若封装成结构体,确保 key 可访问 - 删哈希表用
erase(key),别用erase(iterator)——后者需要先 find,多一次哈希查找 - 如果
Node析构代价高(如含大 buffer),考虑在 erase 前显式 move 出 value,避免析构延迟
为什么不用 std::shared_mutex 的 try_* 版本?
有人想用 try_shared_lock 避免 get() 阻塞,但实际收益极小:shared_lock 几乎不争抢(除非正有 put 在写),而 try 版本要额外检查返回值、处理失败重试逻辑,反而增加分支预测失败概率和代码复杂度。
真正该加 try 的地方是写操作中的“先读后写”竞争:比如 put() 检查是否已存在,若存在则更新值而非插入。这时可以用 try_unique_lock 配合短超时,避免长等;但大多数业务场景下,直接阻塞更简单可靠。
- 99% 的 LRU 使用场景里,shared_lock 不会成为瓶颈,别过早优化
-
try_*适合有明确 SLA 要求的实时系统,普通服务用默认阻塞语义更稳 - 注意:Windows 上 MSVC 的
std::shared_mutex实现曾有性能问题,Clang/GCC 更成熟;若需跨平台且用旧 Windows,考虑封装一层 spin-based fallback
最难缠的不是锁怎么加,而是节点生命周期和迭代器有效性混在一起——稍不注意,splice() 没做对,或者删 list 和删 map 顺序颠倒,core dump 就在下一秒。










