Swift 并发实战:从 Task 到 Actor 重入性,我掉进了“逻辑不一致”的坑

118 阅读3分钟

一、 前言

Swift 5.5 引入的结构化并发(Structured Concurrency)让我们告别了 Completion Handler 的地狱。但你以为用了 TaskActor 就万事大吉了吗?最近在处理一个“账户存款”的 Demo 时,我发现即便代码在编译器看来是“内存安全”的,业务逻辑却依然会崩塌。


二、 概念对撞:Task vs MainActor

  • Task: 异步任务的最小单位。它就像一个轻量级线程,可以挂起和恢复。
  • MainActor: 一个全局独占的执行器,确保代码在主线程运行。它是 DispatchQueue.main.async 的现代声明式替代品。

三、 踩坑演进:一个存款功能的“翻车”现场

第一阶段:初生牛犊(直接加锁思维)

我们想实现一个余额上限为 100 的存款功能。最初的代码长这样:

actor BankAccount {
    private var balance: Double = 0
    
    func deposit(amount: Double) async {
        guard balance < 100 else { return } // 初始检查
        
        let rate = await fetchExchangeRate() // 模拟联网挂起
        
        balance += amount * rate
        print("✅ 存款完成,余额: \(balance)")
    }
}

运行结果: 当并发 5 个存款任务时,最终余额竟然变成了 150.0原因分析:这是典型的 Actor Reentrancy(重入性)。当第一个任务在 await 处挂起时,锁被释放,后续 4 个任务全部通过了 guard balance < 100 的检查。


第二阶段:进阶防御(双重检查锁)

意识到重入性后,我在 await 之后加了二次检查:

func deposit(amount: Double) async {
    guard balance < 100 else { return } 
    
    let rate = await fetchExchangeRate() // 挂起点
    
    // 💡 二次检查:防止挂起期间被别人偷跑
    guard balance < 100 else { 
        print("🚨 余额已达上限,取消存款")
        return 
    }
    balance += amount * rate
}

运行结果: 虽然拦截了一部分,但最终余额依然可能出现 130.0原因分析balance < 100 只能保证当前没满,但不能保证“加完之后”不满。如果当前余额 90,任务 A 存 40,检查通过,结果就变成了 130。内存安全不等于业务原子性。


第三阶段:终极方案(预判式原子检查)

要实现绝对的业务一致性,必须在加法前进行“结果预判”:

func deposit(amount: Double) async {
    let rate = await fetchExchangeRate()
    
    // 💡 架构师思维:计算预期结果并校验
    let projectedBalance = balance + (amount * rate)
    guard projectedBalance <= 100 else {
        print("🚨 严格拦截:存入后将达 \(projectedBalance),已超限!")
        return
    }
    
    balance = projectedBalance
    print("✅ 存款成功,最终余额: \(balance)")
}

四、 深度总结:避坑指南

  1. Actor 不是万能锁:它只保证同一时间只有一个任务修改变量(内存安全),不保证跨越 await 的逻辑连贯性(逻辑原子性)。
  2. await 之后是“新世界”:永远记住,await 回来后,self 的所有状态都可能已经被改过了。
  3. 不要在 await 之前做关键决策:如果业务逻辑依赖于状态,务必在 await 恢复后的第一行重新获取快照或校验。
  4. MainActor 的边界:在 Task 中调用 MainActor 方法必须 await,这是编译器在帮你强制执行线程切换。

五、 结语

Swift 并发模型将很多运行时崩溃变成了编译期错误,这是巨大的进步。但作为开发者,我们不能丧失对并发逻辑的敬畏。Actor 重入性是每一位 iOS 工程师进阶路上的必修课。