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

一、 前言

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


二、 概念对撞:Task vs MainActor

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

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

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

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

swift 复制代码
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 之后加了二次检查:

swift 复制代码
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。内存安全不等于业务原子性。


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

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

swift 复制代码
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 工程师进阶路上的必修课。


相关推荐
LinXunFeng2 小时前
给 Flutter 插件加上 Swift Package Manager 支持
flutter·swift
大龄秃头程序员7 小时前
Swift 面试反直觉:Property Wrapper 真的拿不到宿主 self 吗?
swift
FeliksLv7 小时前
从 +load 到 Swift Macros:自动注册的演进与实践
ios·objective-c·swift
语歌1 天前
AI 语言学习系统的工程边界:为什么 LLM 不该负责复习排期
人工智能·swift
2501_916008892 天前
iOS应用开发工具全面解析:如何选择与优化开发效率
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
大龄秃头程序员2 天前
Swift 属性包装器进阶:从“语法糖”到“并发安全”的 5 个深坑
swift
Lvan的前端笔记3 天前
SwiftUI:iOS 常用视图组件速查
ios·swiftui·swift
智购科技无人售货机工厂3 天前
自动售货机硬件选型避坑:单片机 / 树莓派 / ESP32 怎么选?~YH
人工智能·单片机·嵌入式硬件·r语言·swift·perl·symfony