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