解决 LRU 缓存中的“堆使用后释放”问题

解决 LRU 缓存中的"堆使用后释放"问题

力扣:https://leetcode.cn/problems/lru-cache/?envType=study-plan-v2\&envId=top-100-liked

牛客:https://www.nowcoder.com/practice/5dfded165916435d9defb053c63f1e84?tpId=295\&tqId=2427094\&sourceUrl=%2Fexam%2Foj

一、问题背景

最近在实现一个 LRU(Least Recently Used)缓存时,遇到了一个棘手的问题。LRU 缓存是一种常见的缓存淘汰策略,用于在缓存容量满时,淘汰最长时间未被使用的数据。我的实现使用了 listunordered_map,但在测试时,AddressSanitizer 报告了一个"堆使用后释放"(heap-use-after-free)的错误。这个问题让我困扰了很久,但最终我找到了解决方法,并从中学习到了很多。奇怪的是,同样的代码,牛客测试可以通过,力扣不能通过。

二、问题描述

LRU 缓存的实现中,list 用于维护访问顺序,unordered_map 用于快速查找。list 存储键值对 (key, value)unordered_map 存储键到 list 中节点的迭代器。这样可以快速找到某个键对应的节点,并将其移动到 list 的头部或从尾部移除。

然而,在测试时,AddressSanitizer 报告了一个错误:

复制代码
ERROR: AddressSanitizer: heap-use-after-free on address 0x503000002304 at pc 0x555c98686c24 bp 0x7ffeacde9f10 sp 0x7ffeacde9f08
READ of size 4 at 0x503000002304 thread T0

这个错误表明代码试图访问一个已经被释放的内存地址。问题的根本原因是 unordered_map 中的迭代器在某些情况下失效了。

三、问题分析

(一)get 方法中的问题

get 方法中,代码试图通过 unordered_map 找到某个键对应的节点,并将其移动到 list 的头部:

cpp 复制代码
int value = mp[key]->second;
datalists.erase(mp[key]);
datalists.push_front(make_pair(key, value));

问题在于,datalists.erase(mp[key]) 会移除 list 中的节点,导致 mp[key] 指向的迭代器失效。如果后续代码试图通过 mp[key] 访问节点,就会导致访问已释放内存的错误。

(二)put 方法中的问题

put 方法中,当缓存已满时,会移除 list 的尾部节点:

cpp 复制代码
mp.erase(datalists.back().first);
datalists.pop_back();

这里,datalists.pop_back() 会释放尾部节点的内存。如果在此之前有某个键的迭代器指向了这个尾部节点,那么这个迭代器就会失效。

四、解决方法

(一)修复 get 方法

get 方法中,移除节点后,需要更新 unordered_map 中的迭代器,确保它指向新的节点:

cpp 复制代码
int value = mp[key]->second;
datalists.erase(mp[key]);
datalists.push_front(make_pair(key, value));
mp[key] = datalists.begin();

通过 mp[key] = datalists.begin();,将迭代器更新为指向新插入的节点。

(二)修复 put 方法

put 方法中,移除尾部节点后,需要确保 unordered_map 中没有指向已释放节点的迭代器:

cpp 复制代码
if (mp.find(key) != mp.end()) {
    datalists.erase(mp[key]);
} else if (datalists.size() == cap) {
    mp.erase(datalists.back().first);
    datalists.pop_back();
}
datalists.push_front(make_pair(key, value));
mp[key] = datalists.begin();

通过 mp[key] = datalists.begin();,将迭代器更新为指向新插入的节点。

五、完整的修复代码

以下是修复后的完整代码:

cpp 复制代码
class LRUCache {
public:
    int cap;
    list<pair<int, int>> datalists;
    unordered_map<int, list<pair<int, int>>::iterator> mp;

    LRUCache(int capacity) {
        cap = capacity;
    }

    int get(int key) {
        if (mp.find(key) != mp.end()) {
            int value = mp[key]->second;
            datalists.erase(mp[key]);
            datalists.push_front(make_pair(key, value));
            mp[key] = datalists.begin();
            return value;
        } else {
            return -1;
        }
    }

    void put(int key, int value) {
        if (mp.find(key) != mp.end()) {
            datalists.erase(mp[key]);
        } else if (datalists.size() == cap) {
            mp.erase(datalists.back().first);
            datalists.pop_back();
        }
        datalists.push_front(make_pair(key, value));
        mp[key] = datalists.begin();
    }
};

六、测试用例

为了验证修复后的代码是否正确,我运行了以下测试用例:

cpp 复制代码
LRUCache cache(2);
cache.put(1, 1);
cache.put(2, 2);
cache.get(1);       // 返回 1
cache.put(3, 3);    // 该操作会使得 key=2 作废
cache.get(2);       // 返回 -1 (未找到)
cache.put(4, 4);    // 该操作会使得 key=1 作废
cache.get(1);       // 返回 -1 (未找到)
cache.get(3);       // 返回 3
cache.get(4);       // 返回 4

运行结果表明,修复后的代码能够正确处理各种情况,没有再出现"堆使用后释放"的错误。

七、总结

通过这次经历,我深刻认识到在使用 listunordered_map 时,必须确保迭代器的有效性。在修改 list 的内容后,要及时更新 unordered_map 中的迭代器,避免访问已释放的内存。同时,我也学会了如何使用 AddressSanitizer 来检测内存问题,这对我今后的开发工作非常有帮助。

希望这篇文章能帮助到遇到类似问题的开发者。

相关推荐
观无33 分钟前
Redis远程链接应用案例
数据库·redis·缓存·c#
星星点点洲41 分钟前
【缓存与数据库结合方案】伪从技术 vs 直接同步/MQ方案的深度对比
数据库·缓存
好想有猫猫3 小时前
【Redis】服务端高并发分布式结构演进之路
数据库·c++·redis·分布式·缓存
爱的叹息5 小时前
MyBatis缓存配置的完整示例,包含一级缓存、二级缓存、自定义缓存策略等核心场景,并附详细注释和总结表格
缓存·mybatis
山猪打不过家猪6 小时前
(六)RestAPI 毛子(外部导入打卡/游标分页/Refit/Http resilience/批量提交/Quartz后台任务/Hateoas Driven)
网络·缓存
李宥小哥8 小时前
Redis01-基础-入门
缓存·中间件
多多*9 小时前
非关系型数据库 八股文 Redis相关 缓存雪崩 击穿 穿透
java·开发语言·jvm·数据库·redis·缓存·nosql
伊织code10 小时前
cached-property - 类属性缓存装饰器
python·缓存·cache·装饰器·ttl·property·cached-property
李宥小哥11 小时前
Redis03-基础-C#客户端
开发语言·缓存·中间件·c#
Ten peaches12 小时前
苍穹外卖(缓存商品、购物车)
spring boot·redis·mysql·缓存