前言
在地址映射过程中,若发现所要访问的页面不在内存中,则产生缺页中断。当发生缺页中断时,如果操作系统内存中没有空闲页面,则操作系统必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。而用来选择淘汰哪一页的规则叫做页面置换算法。
一、LRU算法是什么?
LRU 是 Least Recently Used 的缩写,即最近最少使用,是一种常见的页面置换算法。LRU 算法的基本理念是:最近使用的数据在未来一段时间仍会被使用,已经很久没有使用的数据可能在未来较长的一段时间内不会被使用。所以在需要淘汰页面时,每次选择淘汰最久没有被访问的页面。
二、基于 golang 的实现
1.构建结构体
通过 golang 内置的双向链表 list.List 存储每个节点,同时维护一个哈希表,可快速判断需要加载的数据是否已经在链表中存在,无须遍历链表查找,典型的以空间换时间的方式。
type LRU struct {
maxByte int64
curByte int64
list *list.List
cache map[string]*list.Element
mu sync.RWMutex
}
type Entry struct {
Key string
Value []byte
}
func New(maxByte int64) *LRU {
return &LRU{
maxByte: maxByte,
curByte: 0,
list: list.New(),
cache: make(map[string]*list.Element),
}
}
2.读取数据
输入 key 查找数据,直接根据 key 去哈希表中查找,查找成功则将此节点转移至双向链表头部,并返回存储的数据,反之返回查找失败。
func (l *LRU) Get(key string) ([]byte, bool) {
l.mu.RLock()
defer l.mu.RUnlock()
if ele, exist := l.cache[key]; exist {
l.list.MoveToFront(ele)
if entry, ok := ele.Value.(*Entry); ok {
return entry.Value, true
}
}
return nil, false
}
3.淘汰数据
每次淘汰双向链表尾部数据,更新哈希表,删除该节点 key 对应的 value,最后更新 lru 结构体已用字节数。
func (l *LRU) removeOldest() {
ele := l.list.Back()
if ele != nil {
l.list.Remove(ele)
if entry, ok := ele.Value.(*Entry); ok {
delete(l.cache, entry.Key)
l.curByte -= int64(len(entry.Key)) + int64(len(entry.Value))
}
}
}
4.新增数据
数据新增时,需要判断该数据是否已存在,存在则移动到链表头部,并更新使用字节数。不存在则直接插入头部,并更新哈希表。最后根据最大字节数和已使用字节数比较,判断是否需要淘汰数据。
func (l *LRU) Add(key string, value []byte) {
l.mu.Lock()
defer l.mu.Unlock()
if ele, ok := l.cache[key]; ok {
l.list.MoveToFront(ele)
if entry, ok := ele.Value.(*Entry); ok {
l.curByte += int64(len(value)) - int64(len(entry.Value))
entry.Value = value
}
} else {
ele := l.list.PushFront(&Entry{Key: key, Value: value})
l.cache[key] = ele
l.curByte = int64(len(key)) + int64(len(value))
}
for l.maxByte > 0 && l.maxByte < l.curByte {
l.RemoveOldest()
}
}
总结
以上便是此次 golang 实现 lru 算法的全部内容,源代码和测试代码都放在了我的 github 仓库中。github地址:golang 实现 lru 算法,欢迎前往查看,点个 statr 。有问题的话可以评论区讨论。
|