You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

关于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

火山引擎 最新活动