解决 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 来检测内存问题,这对我今后的开发工作非常有帮助。

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

相关推荐
YOU OU6 小时前
Redis基础常识与命令
数据库·redis·缓存
吳所畏惧11 小时前
宝塔面板Redis密码修改指南:SSH命令修改 vs 面板UI界面修改,哪个更靠谱?
运维·服务器·数据库·redis·缓存·ssh
Y38153266216 小时前
SERP API + Redis 缓存层:4 种方案对比与选型
数据库·redis·缓存
海兰18 小时前
【高速缓存】RedisVL 高级查询(全文搜索、混合搜索和 多向量搜索)
数据库·人工智能·redis·缓存
流星白龙1 天前
【Redis】4.基本全局命令
数据库·redis·缓存
流星白龙1 天前
【Redis】3.Redis安装与命令行客户端
数据库·redis·缓存
我不想名字重复2 天前
redis缓存和数据库数据保持一致
数据库·redis·缓存
时空无限2 天前
vllm 大模型启动缓存相关环境变量 export
linux·缓存·vllm
流星白龙2 天前
【Redis】6.计数其他命令
数据库·redis·缓存
正儿八经的少年2 天前
redis 的大 key 和热 key 详解
数据库·redis·缓存