c++17起,std::shared_mutex+std::list+std::unordered_map是线程安全lru缓存最优解:list维护访问序(o(1)增删),map提供o(1)查找,shared_mutex分离读写锁粒度;需完美转发避免拷贝、原子更新迭代器、编译期约束类型安全。

用 std::shared_mutex + std::list + std::unordered_map 组合最稳妥
直接上结论:C++17 起,std::shared_mutex 是线程安全 LRU 缓存的锁粒度最优解,比全用 std::mutex 高效,又比无锁方案稳定。核心结构必须是「双向链表维护访问序 + 哈希表加速查找」,不能反过来——否则每次 get 都要遍历链表,O(n) 就废了。
常见错误是把 std::list::iterator 存进 std::unordered_map 后,没意识到迭代器在 list 发生 splice 或 erase 时仍有效,但若用 vector 或 deque 存节点就会失效。所以必须用 std::list。
-
std::list<:pair value>></:pair>存数据,最新访问放front(),淘汰从back() -
std::unordered_map<key std::list>::iterator></key>提供 O(1) 查找 - 读操作(
get)用shared_lock,写操作(put/evict)用unique_lock
put 时如何避免重复构造和移动开销?
频繁 put 字符串或大对象时,如果接口只接受 const Value&,会强制拷贝;若只接受 Value&&,又没法传左值。正确做法是用完美转发模板:
template <typename V>
void put(const Key& k, V&& v) { ... }
内部用 std::forward<v>(v)</v> 构造节点,再 emplace_front 到 list。这样 string 字面量、临时对象、已存在变量都能零拷贝处理。
立即学习“C++免费学习笔记(深入)”;
容易踩的坑:unordered_map::insert 返回的是 std::pair<iterator bool></iterator>,但如果你先 erase 旧节点再 insert 新节点,中间可能被其他线程 get 到空状态。必须在一个 unique_lock 下原子完成:查旧→删旧→插新→更新 map 迭代器。
为什么不要自己实现 LRU 的「访问计数」或「时间戳」?
有人想用 std::chrono::steady_clock::now() 记最后访问时间,再按时间排序淘汰——这会导致每次 get 都要更新 map 中的时间字段,且 evict 时得遍历整个 map 找最老的,O(n) 不可接受。LRU 的本质是「最近最少使用」,不是「最久未使用」,前者靠访问序链表天然支持 O(1) 淘汰,后者靠时间戳必然退化。
另一个典型错误:用 map<time_point key></time_point> 维护时间序。这引入红黑树 log(n) 开销,还让 key 无法快速反查 value,彻底破坏缓存语义。
- LRU 必须靠「位置」而非「时间」表达使用频次
- 所有淘汰逻辑必须绑定在
list::pop_back()这一动作上,别绕弯 - 如果业务真需要 TTL(过期),那是另一层逻辑,和 LRU 正交,别混进同一套迭代器管理里
编译期限制 key/value 类型能避免哪些运行时陷阱?
比如 Key 必须可哈希、可比较,Value 必须可移动——这些不检查,到运行时插入一个没定义 std::hash 的自定义 struct,编译就挂。应该用 static_assert 卡住:
static_assert(std::is_move_constructible_v<Value>, "Value must be move-constructible"); static_assert(std::is_invocable_r_v<size_t, std::hash<Key>, const Key&>, "Key must be hashable");
更隐蔽的问题是 Key 如果含裸指针或 std::unique_ptr,作为 map 的 key 会因地址变化导致哈希错乱。这时候必须在文档里强调「key 应为值语义类型」,并在构造函数里加 static_assert 拦住带指针成员的类型。
线程安全不等于类型安全。很多崩溃发生在多线程下 key 析构了,但 map 还拿着野指针去调 operator== —— 这类问题只能靠编译期约束提前暴露。











