避免死锁与竞争的最佳实践
Swift的并发模型通过async/await和Actor简化了多线程编程,但数据竞争(Data Race)和死锁(Deadlock)仍是潜在风险。前两节介绍了数据竞争的识别和Thread Sanitizer的使用,本节将总结避免这些问题的最佳实践,从设计到实现提供具体指导。通过这些策略,你将能编写安全、高效的并发代码,防范常见的线程安全隐患。
数据竞争与死锁的回顾
- 数据竞争:多线程无同步访问共享数据,导致结果不可预测。
- 死锁:多个线程互相等待对方释放资源,陷入永久阻塞。
示例(死锁):
actor A {
func callB(_ b: B) async { await b.callA(self) }
}
actor B {
func callA(_ a: A) async { await a.callB(self) }
}
Task {
let a = A()
let b = B()
async let task1 = a.callB(b)
async let task2 = b.callA(a)
await (task1, task2) // 可能死锁
}
- 问题:
A等待B,B等待A,循环依赖导致阻塞。
最佳实践
以下是避免数据竞争和死锁的实用建议:
1. 使用Actor隔离共享状态
策略:将共享数据封装在Actor中,确保串行访问。 实现:
actor Counter {
private var count = 0
func increment() { count += 1 }
func getCount() -> Int { count }
}
Task {
let counter = Counter()
await withTaskGroup(of: Void.self) { group in
for _ in 1...100 {
group.addTask { await counter.increment() }
}
}
print(await counter.getCount()) // 始终为100
}
- 效果:
Actor自动防止竞争,无需锁。
2. 避免直接访问共享变量
策略:禁止多线程直接操作类或结构体的实例变量。 实现:
// 错误:无保护
class UnsafeData {
var value = 0
}
// 正确:用Actor
actor SafeData {
private var value = 0
func setValue(_ newValue: Int) { value = newValue }
}
- 效果:强制通过
Actor接口访问,消除竞争风险。
3. 谨慎处理Actor间的循环依赖
策略:避免Actor互相调用形成闭环,或使用非阻塞设计。 实现(修复死锁):
actor ResourceA {
private var data = 0
func process(_ b: ResourceB) async -> Int {
let bValue = await b.getData() // 不调用B的process
data += bValue
return data
}
}
actor ResourceB {
private var data = 10
func getData() -> Int { data }
}
Task {
let a = ResourceA()
let b = ResourceB()
let result = await a.process(b)
print("结果:\(result)") // 输出:10
}
- 效果:打破循环,避免死锁。
4. 使用结构化并发
策略:优先使用Task Group管理并行任务,减少非结构化复杂性。 实现:
func fetchAllData() async -> [String] {
await withTaskGroup(of: String.self) { group in
for i in 1...3 {
group.addTask { await fetchData(i) }
}
return await group.collectAll()
}
}
func fetchData(_ id: Int) async -> String {
try? await Task.sleep(nanoseconds: 500_000_000)
return "数据\(id)"
}
- 效果:任务关系清晰,自动清理,避免遗留竞争。
5. 检查和响应取消
策略:在长任务中定期检查Task.isCancelled,防止资源浪费。 实现:
Task {
let task = Task {
for i in 1...10 {
try await Task.sleep(nanoseconds: 500_000_000)
try Task.checkCancellation()
print("进度:\(i)")
}
}
try? await Task.sleep(nanoseconds: 2_000_000_000)
task.cancel()
}
- 效果:及时退出,减少死锁或竞争的潜在条件。
6. 最小化锁的使用
策略:若必须使用锁(如遗留代码),确保加锁范围最小化。 实现:
class LockedCounter {
private var count = 0
private let lock = NSLock()
func increment() {
lock.lock()
count += 1
lock.unlock()
}
}
- 效果:减少锁持有时间,避免死锁。
7. 验证线程安全
策略:使用断言或日志确认线程上下文。 实现:
@MainActor
func updateUI(_ text: String) {
assert(Thread.isMainThread)
label.text = text
}
- 效果:运行时验证,避免意外线程访问。
实战案例:库存管理优化
结合第十章的库存案例,应用最佳实践:
actor Inventory {
private var stock: [String: Int] = [:]
func add(id: String, quantity: Int) {
stock[id, default: 0] += quantity
}
func remove(id: String, quantity: Int) throws -> Int {
guard let current = stock[id], current >= quantity else {
throw InventoryError.insufficientStock
}
stock[id] = current - quantity
return current - quantity
}
func getStock() -> [String: Int] {
stock
}
}
enum InventoryError: Error { case insufficientStock }
@MainActor
class StockManager: UIViewController {
private let inventory = Inventory()
func processOrders() async throws {
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask { try await self.inventory.remove(id: "book", quantity: 5) }
group.addTask { await self.inventory.add(id: "pen", quantity: 10) }
try await group.waitForAll()
}
let stock = await inventory.getStock()
print("库存:\(stock)")
}
}
Task {
let manager = StockManager()
try await manager.processOrders()
}
- 实践应用:
Actor隔离库存状态。Task Group结构化并行。- 错误集中处理。
验证与测试
- 运行TSan:确保无竞争警告。
- 压力测试:增加任务数量,验证一致性。
- 日志检查:记录操作顺序,确认无死锁。
小结
避免数据竞争和死锁需要从设计到实现的全方位考虑。本节通过最佳实践,如使用Actor、结构化并发和取消检查,提供了防范并发问题的策略。结合库存案例,这些方法展示了如何构建健壮的异步代码。本章回顾了竞争与调试的全流程,下一章将探讨与遗留代码的集成,进一步扩展你的并发应用能力。
内容说明
- 结构:从回顾到最佳实践,再到案例和总结。
- 代码:包含死锁修复和库存优化示例,突出实用性。
- 语气:指导性且总结性,适合技术书籍收尾章节。
- 衔接:承接前两节(竞争识别和TSan),预告后续(遗留代码)。
