题目
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 隔离:所有对
store和metadata的访问都通过 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标记只读计算属性避免跳线程 - 内存安全:对引用类型属性使用
weak或unowned - 错误处理:在 Actor 内实现
try逻辑统一处理错误
常见错误
- 在 Actor 外部直接修改状态(编译错误)
- 值类型包含引用类型导致写时复制失效
- 忘记实现缓存淘汰策略引发内存泄漏
- 循环引用导致 Actor 无法释放
扩展知识
- GlobalActor:对现有类添加线程安全层(如
@MainActor) - Sendable 协议:确保跨域传递的类型安全
- Actor reentrancy:await 期间可能发生状态变化,需用快照保持一致性
- 性能对比:Actor vs 锁(OSAllocatedUnfairLock)的性能差异