关于Herb Sutter的10行缓存代码缺少清理机制的技术咨询
解决Herb Sutter经典缓存代码的内存清理问题
嘿,刚好我对这段Herb Sutter那大名鼎鼎的10行缓存代码熟得很,咱们来聊聊怎么解决它的内存清理痛点~
先把这段代码贴出来方便回顾:
shared_ptr<widget> get_widget(int id) { static map<int, weak_ptr<widget>> cache; static mutex m; lock_guard<mutex> hold(m); auto sp = cache[id].lock(); if (!sp) cache[id] = sp = load_widget(id); return sp; }
正如你提到的,这段代码最大的槽点就是缓存里的weak_ptr永远不会被移除——哪怕对应的widget早就被销毁了,map里还是留着没用的key和空weak_ptr,时间长了会占不少内存,确实是个问题。
下面给你几个实用的清理方案,按需选择:
方案一:每次访问时顺带清理过期条目
这种实现最简单,不需要额外线程,就在获取锁之后、处理缓存之前,遍历map删掉那些已经过期的weak_ptr就行:shared_ptr<widget> get_widget(int id) { static map<int, weak_ptr<widget>> cache; static mutex m; lock_guard<mutex> hold(m); // 清理过期的缓存条目 auto it = cache.begin(); while (it != cache.end()) { if (it->second.expired()) { it = cache.erase(it); } else { ++it; } } auto sp = cache[id].lock(); if (!sp) cache[id] = sp = load_widget(id); return sp; }优点是零额外依赖,实现快;缺点是如果缓存条目特别多,每次遍历清理会拖慢
get_widget的响应速度,适合缓存规模不大的场景。方案二:后台线程定时清理
如果缓存条目多,不想影响主业务的响应速度,可以开一个后台线程,定期加锁清理过期条目:#include <thread> #include <chrono> #include <atomic> // 全局退出标志,用于优雅终止清理线程 std::atomic<bool> g_exit_cleaner(false); shared_ptr<widget> get_widget(int id) { static map<int, weak_ptr<widget>> cache; static mutex m; // 只启动一次后台清理线程 static std::thread cleaner([](){ while (!g_exit_cleaner.load()) { // 每分钟清理一次,可根据需求调整间隔 std::this_thread::sleep_for(std::chrono::minutes(1)); std::lock_guard<std::mutex> hold(m); auto it = cache.begin(); while (it != cache.end()) { if (it->second.expired()) { it = cache.erase(it); } else { ++it; } } } }); lock_guard<mutex> hold(m); auto sp = cache[id].lock(); if (!sp) cache[id] = sp = load_widget(id); return sp; } // 程序退出前调用,终止清理线程 void cleanup() { g_exit_cleaner.store(true); static_cast<std::thread&>(cleaner).join(); }这里加了个退出标志,避免程序退出时线程还在跑导致资源泄漏。这种方案对主业务的影响最小,适合缓存规模大、对响应速度要求高的场景。
方案三:用支持高效清理的容器(比如Boost Multi-Index)
如果你的项目已经在用Boost库,那可以用multi_index_container来优化清理效率。它能同时按id索引和按weak_ptr的过期状态排序,清理时不用遍历整个容器:#include <boost/multi_index_container.hpp> #include <boost/multi_index/ordered_index.hpp> #include <boost/multi_index/member.hpp> struct CacheEntry { int id; std::weak_ptr<widget> wp; }; namespace bmi = boost::multi_index; using Cache = bmi::multi_index_container< CacheEntry, bmi::indexed_by< // 按id唯一索引,用于快速查找 bmi::ordered_unique<bmi::member<CacheEntry, int, &CacheEntry::id>>, // 按weak_ptr和id排序,方便快速清理过期条目 bmi::ordered_non_unique< bmi::composite_key< CacheEntry, bmi::member<CacheEntry, std::weak_ptr<widget>, &CacheEntry::wp>, bmi::member<CacheEntry, int, &CacheEntry::id> > > > >; shared_ptr<widget> get_widget(int id) { static Cache cache; static std::mutex m; std::lock_guard<std::mutex> hold(m); // 清理过期条目,只需要遍历到第一个未过期的就停 auto& expiry_idx = cache.get<1>(); auto it = expiry_idx.begin(); while (it != expiry_idx.end() && it->wp.expired()) { it = expiry_idx.erase(it); } auto id_idx = cache.get<0>().find(id); std::shared_ptr<widget> sp; if (id_idx != cache.get<0>().end()) { sp = id_idx->wp.lock(); } if (!sp) { sp = load_widget(id); cache.get<0>().insert({id, sp}); } return sp; }这种方案清理效率最高,适合大型缓存场景,不过需要依赖Boost库。
总的来说,核心思路就是定期或者在访问时检查并移除过期的weak_ptr条目,具体选哪种方案,看你的缓存规模、性能要求以及项目依赖情况就行~
内容的提问来源于stack exchange,提问作者Ben




