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

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

相关推荐
2301_793086871 天前
Redis 04 Reactor
数据库·redis·缓存
189228048611 天前
NY243NY253美光固态闪存NY257NY260
大数据·网络·人工智能·缓存
青鱼入云1 天前
redis怎么做rehash的
redis·缓存
FFF-X1 天前
Vue3 路由缓存实战:从基础到进阶的完整指南
vue.js·spring boot·缓存
夜影风2 天前
Nginx反向代理与缓存实现
运维·nginx·缓存
编程(变成)小辣鸡2 天前
Redis 知识点与应用场景
数据库·redis·缓存
菜菜子爱学习3 天前
Nginx学习笔记(八)—— Nginx缓存集成
笔记·学习·nginx·缓存·运维开发
魏波.3 天前
常用缓存软件分类及详解
缓存
yh云想3 天前
《多级缓存架构设计与实现全解析》
缓存·junit
白仑色3 天前
Redis 如何保证数据安全?
数据库·redis·缓存·集群·主从复制·哨兵·redis 管理工具