侧边栏壁纸
博主头像
colo

欲买桂花同载酒

  • 累计撰写 1823 篇文章
  • 累计收到 0 条评论

Swift 中如何设计线程安全的缓存系统?结合值类型、引用类型和 Actor 进行实现

2025-12-11 / 0 评论 / 4 阅读

题目

Swift 中如何设计线程安全的缓存系统?结合值类型、引用类型和 Actor 进行实现

信息

  • 类型:问答
  • 难度:⭐⭐⭐

考点

值类型与引用类型的区别,Swift Actor 模型,线程安全设计,内存管理,协议扩展

快速回答

实现线程安全的缓存系统需要:

  • 使用 actor 封装核心缓存状态,确保串行访问
  • 值类型存储缓存项实现写时复制,避免意外共享
  • 引用类型包装缓存元数据,使用弱引用避免循环引用
  • 结合 async/await 处理异步访问
  • 实现缓存淘汰策略(如 LRU)
## 解析

问题核心

在并发环境下设计缓存系统需要解决:1)多线程同时读写的数据竞争 2)内存管理 3)性能优化。Swift 的值类型、引用类型和 Actor 各有适用场景:

  • 值类型(struct):线程安全基础,写时复制特性适合存储缓存项
  • 引用类型(class):适合共享元数据,需注意循环引用
  • Actor:解决数据竞争的核心机制,保证状态隔离

完整实现方案

// 缓存项:值类型实现写时复制
struct CacheItem<Value: Sendable> {
    let value: Value
    var lastAccessed: Date
}

// 缓存元数据:引用类型使用弱引用
class CacheMetadata {
    weak var owner: CacheManager?
    var hits: Int = 0
}

// 核心缓存 Actor
actor CacheManager<Key: Hashable & Sendable, Value: Sendable> {
    private var store: [Key: CacheItem<Value>] = [:]
    private var metadata: [Key: CacheMetadata] = [:]
    private let maxSize: Int

    init(maxSize: Int = 100) {
        self.maxSize = maxSize
    }

    // 线程安全的读写方法
    func setValue(_ value: Value, forKey key: Key) {
        store[key] = CacheItem(value: value, lastAccessed: Date())
        metadata[key] = CacheMetadata()
        metadata[key]?.owner = self
        evictIfNeeded()
    }

    func getValue(forKey key: Key) -> Value? {
        guard var item = store[key] else { return nil }
        item.lastAccessed = Date()  // 结构体写时复制在此发生
        store[key] = item
        metadata[key]?.hits += 1
        return item.value
    }

    // LRU 淘汰策略
    private func evictIfNeeded() {
        guard store.count > maxSize else { return }
        let oldest = store.min { $0.value.lastAccessed < $1.value.lastAccessed }?.key
        oldest.map { store[$0] = nil }
    }
}

关键设计原理

  • Actor 隔离:所有对 storemetadata 的访问都通过 Actor 串行化
  • 值类型优势CacheItem 使用 struct,修改 lastAccessed 时触发写时复制,避免污染其他线程
  • 引用类型控制CacheMetadata 使用 class 并采用弱引用打破循环引用
  • Sendable 约束:确保跨线程传递的类型是安全的

使用示例

Task {
    let cache = CacheManager<String, [UIImage]>()

    // 并发写入
    async let write1 = cache.setValue(images1, forKey: "user1")
    async let write2 = cache.setValue(images2, forKey: "user2")

    // 并发读取
    async let read1 = cache.getValue(forKey: "user1")
    async let read2 = cache.getValue(forKey: "user2")

    _ = await [write1, write2, read1, read2]
}

最佳实践

  • 容量控制:实现 LRU/TTL 等淘汰策略防止内存增长
  • 性能优化:使用 nonisolated 标记只读计算属性避免跳线程
  • 内存安全:对引用类型属性使用 weakunowned
  • 错误处理:在 Actor 内实现 try 逻辑统一处理错误

常见错误

  • 在 Actor 外部直接修改状态(编译错误)
  • 值类型包含引用类型导致写时复制失效
  • 忘记实现缓存淘汰策略引发内存泄漏
  • 循环引用导致 Actor 无法释放

扩展知识

  • GlobalActor:对现有类添加线程安全层(如 @MainActor
  • Sendable 协议:确保跨域传递的类型安全
  • Actor reentrancy:await 期间可能发生状态变化,需用快照保持一致性
  • 性能对比:Actor vs 锁(OSAllocatedUnfairLock)的性能差异