线程安全的LRU实现

news/2025/4/2 23:21:20/文章来源:https://www.cnblogs.com/smartljy/p/18804252

线程安全的LRU实现

无清理线程


实现方案

根据需求,我们需要在原有LRU缓存基础上增加线程安全过期时间,并确保写入不阻塞读取。以下是分步骤的代码实现和解释:


一、增加线程安全(读写锁)

使用 std::shared_mutex(读写锁)保护共享资源,确保:

  • 读操作(get:允许多个线程同时读取。
  • 写操作(put、节点移动、过期删除):需要独占访问。
#include <shared_mutex>
#include <chrono>class LRUCache {
private:mutable std::shared_mutex mutex_;  // 读写锁// ... 其他原有成员
};

二、添加过期时间

1. 修改节点结构

每个节点记录过期时间(当前时间 + TTL)。

struct DLinkedNode {int key, value;DLinkedNode* prev;DLinkedNode* next;std::chrono::steady_clock::time_point expire_time;  // 过期时间DLinkedNode(): key(0), value(0), prev(nullptr), next(nullptr) {}DLinkedNode(int _key, int _value, std::chrono::steady_clock::time_point _expire_time): key(_key), value(_value), prev(nullptr), next(nullptr), expire_time(_expire_time) {}
};

2. 检查是否过期

bool isExpired(DLinkedNode* node) const {return std::chrono::steady_clock::now() > node->expire_time;
}

三、写入操作不阻塞读取

1. get 方法逻辑

  • 读锁:查找节点。
  • 惰性删除:若节点过期,升级为写锁并删除。
  • 移动节点:若未过期,升级为写锁并移动到头部。
int get(int key) {// 1. 读锁查找节点std::shared_lock<std::shared_mutex> read_lock(mutex_);auto it = cache.find(key);if (it == cache.end()) return -1;DLinkedNode* node = it->second;// 2. 检查是否过期if (isExpired(node)) {read_lock.unlock();  // 释放读锁,准备加写锁std::unique_lock<std::shared_mutex> write_lock(mutex_);// 双重检查防止竞态条件if (cache.count(key) && isExpired(cache[key])) {removeNode(cache[key]);cache.erase(key);delete node;size--;}return -1;}// 3. 未过期则移动节点到头部(需写锁)read_lock.unlock();std::unique_lock<std::shared_mutex> write_lock(mutex_);// 再次检查节点状态if (!cache.count(key) || isExpired(cache[key])) return -1;moveToHead(cache[key]);return cache[key]->value;
}

2. put 方法逻辑

  • 写锁:插入新节点或更新已有节点。
  • 设置过期时间:例如TTL=1秒。
void put(int key, int value) {auto expire_time = std::chrono::steady_clock::now() + std::chrono::seconds(1);std::unique_lock<std::shared_mutex> write_lock(mutex_);// ... 原有逻辑修改为使用带过期时间的节点if (!cache.count(key)) {DLinkedNode* node = new DLinkedNode(key, value, expire_time);// ... 其他逻辑不变} else {cache[key]->value = value;cache[key]->expire_time = expire_time;moveToHead(cache[key]);}
}

四、关键设计点

1. 读写锁的精细控制

  • 读锁(std::shared_lock:用于只读操作(哈希表查找、节点访问)。
  • 写锁(std::unique_lock:用于修改数据结构(插入、删除、移动节点)。

2. 惰性过期删除

  • 优点:无需后台线程,仅在访问时清理过期数据。
  • 缺点:内存释放不够及时,但适合低频率过期场景。

3. 避免阻塞的关键

  • 锁升级策略:在get中先释放读锁再加写锁,确保写入操作不长时间阻塞读操作。
  • 双重检查(Double-Check):防止在释放读锁后,数据状态被其他线程修改。

五、完整代码示例

#include <unordered_map>
#include <memory>
#include <shared_mutex>
#include <chrono>struct DLinkedNode {int key, value;DLinkedNode* prev;DLinkedNode* next;std::chrono::steady_clock::time_point expire_time;DLinkedNode(int _key, int _value, std::chrono::steady_clock::time_point _expire_time): key(_key), value(_value), prev(nullptr), next(nullptr), expire_time(_expire_time) {}
};class LRUCache {
private:std::unordered_map<int, DLinkedNode*> cache;DLinkedNode* head;DLinkedNode* tail;int size;int capacity;mutable std::shared_mutex mutex_;bool isExpired(DLinkedNode* node) const {return std::chrono::steady_clock::now() > node->expire_time;}void addToHead(DLinkedNode* node) {node->prev = head;node->next = head->next;head->next->prev = node;head->next = node;}void removeNode(DLinkedNode* node) {node->prev->next = node->next;node->next->prev = node->prev;}void moveToHead(DLinkedNode* node) {removeNode(node);addToHead(node);}DLinkedNode* removeTail() {DLinkedNode* node = tail->prev;removeNode(node);return node;}public:LRUCache(int _capacity) : capacity(_capacity), size(0) {head = new DLinkedNode(-1, -1, std::chrono::steady_clock::now());tail = new DLinkedNode(-1, -1, std::chrono::steady_clock::now());head->next = tail;tail->prev = head;}~LRUCache() {// 清理内存(示例代码省略)}int get(int key) {std::shared_lock<std::shared_mutex> read_lock(mutex_);auto it = cache.find(key);if (it == cache.end()) return -1;DLinkedNode* node = it->second;if (isExpired(node)) {read_lock.unlock();std::unique_lock<std::shared_mutex> write_lock(mutex_);if (cache.count(key) && isExpired(cache[key])) {removeNode(cache[key]);cache.erase(key);delete node;size--;}return -1;}read_lock.unlock();std::unique_lock<std::shared_mutex> write_lock(mutex_);if (!cache.count(key) || isExpired(cache[key])) return -1;moveToHead(cache[key]);return cache[key]->value;}void put(int key, int value) {auto expire_time = std::chrono::steady_clock::now() + std::chrono::seconds(1);std::unique_lock<std::shared_mutex> write_lock(mutex_);if (!cache.count(key)) {DLinkedNode* node = new DLinkedNode(key, value, expire_time);cache[key] = node;addToHead(node);size++;if (size > capacity) {DLinkedNode* removed = removeTail();cache.erase(removed->key);delete removed;size--;}} else {cache[key]->value = value;cache[key]->expire_time = expire_time;moveToHead(cache[key]);}}
};

六、总结

  • 线程安全:通过读写锁(std::shared_mutex)实现高效并发访问。
  • 过期时间:节点记录过期时间,访问时惰性删除。
  • 无阻塞读取:读操作仅在必要时升级为写锁,且锁持有时间极短。

加入清理线程


问题分析

当使用惰性删除(仅在访问时检查过期)时,若大量节点长时间未被访问但已过期,会导致缓存体积膨胀。我们需要增加主动删除机制,定期或按策略清理过期数据。


解决方案

以下是两种主要方法,可根据场景选择或结合使用:


方案一:后台清理线程

启动一个独立线程定期扫描并删除过期节点,确保内存及时释放。

1. 代码修改

class LRUCache {
private:// ... 原有成员std::thread cleanup_thread;        // 后台清理线程std::atomic<bool> running{true};   // 控制线程退出std::condition_variable cv;        // 用于定时或条件触发std::mutex cleanup_mutex;          // 条件变量专用锁void backgroundCleanup() {while (running) {{std::unique_lock<std::mutex> lock(cleanup_mutex);// 每隔N秒触发一次清理(例如5秒)cv.wait_for(lock, std::chrono::seconds(5), [this] { return !running; });if (!running) break;}// 执行清理performExpirationCheck();}}void performExpirationCheck() {std::unique_lock<std::shared_mutex> write_lock(mutex_);auto now = std::chrono::steady_clock::now();DLinkedNode* node = tail->prev;while (node != head) {  // 遍历双向链表DLinkedNode* prev_node = node->prev;if (now > node->expire_time) {removeNode(node);cache.erase(node->key);delete node;size--;}node = prev_node;}}public:LRUCache(int _capacity) : capacity(_capacity), size(0) {// ... 原有初始化cleanup_thread = std::thread(&LRUCache::backgroundCleanup, this);}~LRUCache() {running = false;cv.notify_all();  // 唤醒线程以便退出if (cleanup_thread.joinable()) {cleanup_thread.join();}// ... 原有析构逻辑}
};

2. 关键设计点

  • 定时清理:后台线程每隔固定时间(如5秒)遍历链表删除过期节点。
  • 线程协调
    • 使用 std::condition_variable 实现定时或条件触发。
    • running 原子变量控制线程优雅退出。
  • 锁的粒度
    • 清理时持有写锁unique_lock),阻塞其他写操作,但读操作仍可并发。
    • 遍历方向:从链表尾部(LRU端)开始,因过期概率更高。

3. 优缺点

  • 优点:内存释放及时。
  • 缺点
    • 遍历整个链表可能耗时,尤其当容量大时。
    • 持有写锁期间会阻塞所有写操作(但读操作仍可用共享锁)。

方案二:批量惰性删除

写操作(put中附加清理逻辑,每次删除固定数量(如5个)过期节点,逐步释放内存。

1. 代码修改

void put(int key, int value) {auto expire_time = std::chrono::steady_clock::now() + std::chrono::seconds(1);std::unique_lock<std::shared_mutex> write_lock(mutex_);// 附加清理逻辑:每次put时清理最多5个过期节点int cleanup_count = 0;DLinkedNode* node = tail->prev;auto now = std::chrono::steady_clock::now();while (node != head && cleanup_count < 5) {DLinkedNode* prev_node = node->prev;if (now > node->expire_time) {removeNode(node);cache.erase(node->key);delete node;size--;cleanup_count++;}node = prev_node;}// ... 原有put逻辑
}

2. 关键设计点

  • 增量清理:每次put时从尾部向前清理最多5个过期节点。
  • 锁复用:写操作已持有写锁,清理无需额外加锁。
  • 清理方向:从尾部(LRU端)开始,过期概率更高。

3. 优缺点

  • 优点
    • 无额外线程开销。
    • 清理操作分散到每次put中,避免集中式耗时。
  • 缺点
    • 清理不完全,极端情况下仍可能膨胀。
    • 写操作可能因附加清理逻辑略微变慢。

方案三:分层过期检查

结合上述两种方案,根据系统负载动态调整清理频率。

1. 动态调整策略

  • 低负载时:使用后台线程定期清理。
  • 高负载时:增加put中的批量清理数量(例如每次清理10个节点)。

2. 代码示例

// 在LRUCache类中增加负载状态跟踪
std::atomic<int> put_counter{0};
void put(int key, int value) {// ... 原有逻辑put_counter++;// 动态调整清理数量int cleanup_limit = (put_counter.load() > 1000) ? 10 : 5;int cleanup_count = 0;DLinkedNode* node = tail->prev;auto now = std::chrono::steady_clock::now();while (node != head && cleanup_count < cleanup_limit) {// ... 清理逻辑}put_counter.store(0);
}

总结

方案 适用场景 实现复杂度 内存控制及时性
后台线程 内存敏感,允许短暂写阻塞
批量惰性删除 写操作频繁,容忍一定内存膨胀
分层策略 负载波动大,需平衡性能与内存 中高

根据实际需求选择方案:

  • 严格内存限制:方案一(后台线程) + 方案二(批量清理)。
  • 高吞吐优先:方案二(批量清理) + 动态调整。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/909539.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

工业通信协议“牵手密码”,Ethernet IP转Profinet网关的桥梁魔法

在当前工业自动化领域,实时以太网技术已经成为至关重要的通信标准之一。Profinet和EtherNetIP作为两种广泛采用的实时以太网协议,各自拥有其独特的性能优势和适用场景。本文旨在探讨稳联技术Profinet转EtherNetIP网关WL-PN-EIPM的功能,并评估其在节能实施与监测方面的应用价…

LeetCode刷题-动态规划-爬楼梯

LeetCode刷题-动态规划-爬楼梯 题目: 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢? 示例 1: 输入:n = 2 输出:2 解释:有两种方法可以爬到楼顶。1 阶 + 1 阶 2 阶 示例 2:输入:n = 3 输出:3 解释:有…

【攻防世界】Hidden-Message

⭕、知识点 流量分析/端口号隐写/tshark/json文件处理 一、题目二、解法 1、端口号个位呈现有规律的01交替,可能隐藏信息。 2、为便于提取信息,使用kali的tshark对其进行转存 tshark -r input.pcap -T json > output.txt注意在使用tshark时应避免使用root账户 否则会出现如…

022 props组件交互

.vue 的文件,就是一个组件,每个.vue 文件就是每个页面html 的时候,每个页面都是一个 htmlvue2 和 vue3 的生命周期钩子是不同的components:常用的组件,公共的组件views:用来存放页面的新建项目,删除HelloWorld.vue components也删除views删除 这个index.js删除 这两页面…

客户端打开BI报表提示 Your current browser is not supported”

win7的打开会报这个问题, win11可以正常打开, 应该是环境差异导致。

Linux-常用命令(3)

Linux-常用命令(3)Linux常用命令 查看文件 cat命令 cat命令可以创建一个或者多个文件、查看文件内容、连接文件,常用于查看文件内容 cat 文件名 //显示文件内容 cat -n 文件名 //显示文件内容,并显示行号 cat - 文件名 //显示文件内容(包括不可见字符)系统时间 date命令…

【EI】机器人与传感器网络国际会议(RoSeN 2025)

第一届机器人与传感器网络国际会议(RoSeN 2025)将于2025年5月16-18日在贵阳举行,会议将围绕机器人展开的在机器人、人机交互、传感、智能控制等相关研究领域,邀请国内外数位在此领域学术卓越的学者专家做相关致辞与报告,共同探讨机器人发展最新发展方向及行业前沿动态。会…

[转]玩客云刷armbian后根目录扩展

地址:玩客云刷armbian后根目录扩展_IT码迹最近拼夕夕搞了个玩客云,自己懒得刷机(太麻烦,还要绝育什么的)所以直接买的刷好的,商家送了个U盘32G已经做好了镜像。 商家镜像刷了不少东西除了openwrt,其他几个docker镜像都是armbian比较好用的。不过在我要安装其他插件的时候发…

生成未来:解码智能技术驱动的产业革命

在人工智能浪潮的推动下,AI生图与视频技术正以惊人的速度重塑人类的生产方式。从一张图片的生成到一段视频的秒级渲染,技术的突破不仅解放了生产力,更催生了全新的商业生态。这场变革的核心,在于用算法替代重复劳动,以智能激发无限创意,而这一切仅仅是开端。 一、技术突破…

云终端远程自动调用开关机功能

云桌面项目由于缺少一键关机和开机功能,通过Linux实现自动化调用开机和关机 1、收集所有终端信息的MAC地址收集方式可以采用ipscan25.exe也可以通过cmd下arp -a方式收集MAC地址,同时记录MAC可以IP地址的对应关系。2、所有终端安装openssh使用系统自带或者下载OpenSSH-Win64-v…

20242801 2024-2025-2 《网络攻防实践》第5次作业

一、实验内容 ​ 配置linux系统防火墙,并设置相关过滤规则;使用snort入侵检测工具进行离线扫描,并分析生成的报警日志。分析Honeywell的防火墙和IDS/IPS配置规则。 二、实验过程 (一)防火墙配置 1、过滤ICMP包 ​ ping命令通过设置icmp实现,所以我们使用ping命令来验证li…

“电脑玩手机神器Scrcpy!投屏/录屏/打游戏,1分钟搞定安装教程”

前言 什么是 Scrcpy?Scrcpy 是一款开源的 Android 屏幕镜像与控制工具,由 Genymobile 开发。它可以通过 USB 或 WiFi 将 Android 设备的屏幕实时显示到电脑上,并允许通过电脑的键盘和鼠标直接操作 Android 设备。 scrcpy 能帮你干啥?在电脑上玩手机——刷抖音、打游戏、聊微…