【Swift】LRU 算法

765 阅读2分钟

Overivew

LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

image.png

  1. 新数据插入到链表头部;
  2. 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
  3. 当链表满的时候,将链表尾部的数据丢弃。

image.png

其过程如下:

  1. 最开始时,内存空间是空的,因此依次进入A、B、C是没有问题的
  2. 当加入D时,就出现了问题,内存空间不够了,因此根据LRU算法,内存空间中A待的时间最为久远,选择A,将其淘汰
  3. 当再次引用B时,内存空间中的B又处于活跃状态,而C则变成了内存空间中,近段时间最久未使用的
  4. 当再次向内存空间加入E时,这时内存空间又不足了,选择在内存空间中待的最久的C将其淘汰出内存,这时的内存空间存放的对象就是E->B->D

Swift 实现

可以看看,github.com/nicklockwoo… 的实现

创建一个Container,这是一个双端队列

final class Container {
    var value: Value
    var cost: Int
    let key: Key
    unowned(unsafe) var prev: Container?
    unowned(unsafe) var next: Container?

    init(value: Value, cost: Int, key: Key) {
        self.value = value
        self.cost = cost
        self.key = key
    }
}

定义其删除节点和添加节点操作

// Remove container from list
func remove(_ container: Container) {
    if head === container { //如果待删除节点等于头节点,则头部节点向下移动
        head = container.next
    }
    if tail === container {//如果待删除节点等于尾节点
        tail = container.prev
    }
    // 清空无效链接
    container.next?.prev = container.prev
    container.prev?.next = container.next
    container.next = nil
}

// Append container to list 将节点加到链表尾部
func append(_ container: Container) {
    assert(container.next == nil)
    if head == nil {
        head = container
    }
    container.prev = tail
    tail?.next = container
    tail = container
}

还有清空链表内容, 注意这里的lock

func clean() {
    lock.lock()
    defer { lock.unlock() }
    while totalCost > totalCostLimit || count > countLimit,
          let container = head
    {
        remove(container)
        values.removeValue(forKey: container.key)
        totalCost -= container.cost
    }
}

下面看一下整体的构造,

private var values: [Key: Container] = [:]
private unowned(unsafe) var head: Container?
private unowned(unsafe) var tail: Container?
var count: Int { values.count }
var isEmpty: Bool { values.isEmpty }

其中,values包括数据的key和指向对应的队列节点 当有新数据要加入缓存时,调用setValue,

func setValue(_ value: Value?, forKey key: Key, cost: Int = 0) {
    guard let value = value else {  // 如果存在
        removeValue(forKey: key)
        return
    }
    lock.lock()
    if let container = values[key] {
        container.value = value
        totalCost -= container.cost
        container.cost = cost
        remove(container)
        append(container)
    } else {
        let container = Container(
            value: value,
            cost: cost,
            key: key
        )
        values[key] = container
        append(container)
    }
    totalCost += cost
    lock.unlock()
    clean()
}

当要删除某个数据时,

func removeValue(forKey key: Key) -> Value? {
    lock.lock()
    defer { lock.unlock() }
    guard let container = values.removeValue(forKey: key) else {
        return nil
    }
    remove(container)
    totalCost -= container.cost
    return container.value
}

里面加入锁机制 是为了保证LRUCache的并发。