直接用 std::unordered_map + std::list 实现 LRU 缓存线程不安全,因其非线程安全且 get() 需修改 list 顺序,所有操作必须由同一 mutex 保护,避免迭代器失效、崩溃或状态不一致。

为什么直接用 std::unordered_map + std::list 会线程不安全
因为 std::unordered_map 和 std::list 本身不是线程安全容器,多个线程同时调用 get() 和 put() 时,可能触发迭代器失效、节点重复插入、或链表指针错乱。典型现象是程序随机崩溃、Segmentation fault,或缓存命中率异常归零。
关键点在于:哪怕只读操作(如 get())也要更新访问顺序——这必须修改链表结构,因此不能简单加个 const 就认为“只读”;所有涉及 map 查找+插入+删除、以及 list 的 splice() 或 erase(),都必须受同一把锁保护。
- 不要为
get()和put()分别加不同互斥锁——容易引发死锁或状态不一致 - 避免在锁内做耗时操作(如深拷贝大对象),否则吞吐量骤降
-
std::shared_mutex在这里没用:因为get()不是纯读,它要改顺序,所以写锁不可避免
用 std::mutex 包裹整个操作是最稳妥的起点
对绝大多数中小规模服务(QPS std::mutex 足够,且代码清晰、不易出错。重点是把所有共享状态的访问都包进去:map 查找、list 重排序、容量检查与淘汰。
class LRUCache {
std::unordered_map<int std::list int>>::iterator> cache_;
std::list<:pair int>> order_;
int capacity_;
mutable std::mutex mtx_;
public:
LRUCache(int capacity) : capacity_(capacity) {}
int get(int key) {
std::lock_guard<:mutex> lock(mtx_);
auto it = cache_.find(key);
if (it == cache_.end()) return -1;
// 把对应节点移到 list 前端
order_.splice(order_.begin(), order_, it->second);
return it->second->second;
}
void put(int key, int value) {
std::lock_guard<:mutex> lock(mtx_);
auto it = cache_.find(key);
if (it != cache_.end()) {
it->second->second = value;
order_.splice(order_.begin(), order_, it->second);
} else {
if (cache_.size() >= static_cast<size_t>(capacity_)) {
// 淘汰尾部
auto last = order_.back();
cache_.erase(last.first);
order_.pop_back();
}
order_.emplace_front(key, value);
cache_[key] = order_.begin();
}
}
};</size_t></:mutex></:mutex></:pair></int>
-
order_.splice()是 O(1),别用erase()+push_front()——后者会构造新节点,且破坏原有迭代器有效性 -
cache_存的是list::iterator,不是指针或索引,确保能 O(1) 定位并移动节点 - 构造函数不加锁,因为此时对象还没被多线程访问;但所有成员函数必须加锁
当性能瓶颈出现时,才考虑分段锁或 std::shared_mutex 优化
如果实测发现锁争用严重(比如 top 中 pthread_mutex_lock 占 CPU 高),再升级。注意:不是所有“高并发”场景都需要优化——先压测,再判断。
常见误区是过早引入复杂方案:比如按 key 哈希分桶后每桶一把锁,但 LRU 的全局顺序性要求每次访问都要调整位置,分桶后无法保证整体 LRU 正确性;或者误以为 std::shared_mutex 能提升 get() 性能,结果发现仍需写锁更新顺序,收益极小。
- 真正有效的优化是减少锁持有时间:把 value 拷贝(如
std::string)移出锁区,只在锁内做 map/list 操作 - 若 value 很大,考虑存储
std::shared_ptr而非值本身,避免锁内拷贝 - 不要用
std::atomic替代 mutex——原子变量无法协调 map 和 list 两个数据结构的一致性
测试线程安全不能只靠 “跑得通”,要看竞争逻辑是否覆盖
写个测试,让多个线程反复 get() 和 put() 同一批 key,持续几秒,然后验证:缓存大小始终 ≤ capacity、get() 返回值正确、无崩溃或 ASan 报错。单纯单线程测试完全无效。
- 用
std::thread启动 4–8 个线程比用std::async更易暴露竞态 - 加入随机 sleep(微秒级)可增加调度不确定性,更容易触发问题
- 务必开启 AddressSanitizer 和 ThreadSanitizer 编译:
-fsanitize=address,thread
LRU 的线程安全本质是“状态一致性”而非“快慢”,最易忽略的是:淘汰逻辑和插入逻辑之间没有原子边界,必须靠锁强制串行。一旦漏掉某处访问,问题往往延迟暴露,排查成本远高于初期加锁。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











