Swift 一个小型游戏对象模型渐进式设计(五)——Swift 并发世界:把 Attackable 搬进 actor

为什么"并发"突然成了刚需

真实场景里:

  • 游戏服务器:32 条网络线程并发处理玩家技能;
  • 客户端:主线程发动画,后台线程算伤害,Timer 触发 dot;
  • 单机多核:SceneKit 物理回调、Vision 识别、Swift Concurrency Task 同时读写同一 BOSS 的血量。

如果还用传统锁:

swift 复制代码
objc_sync_enter(self)
hp -= damage
objc_sync_exit(self)

轻则性能抖动,重则死锁;而 Swift 5.5 起的 Actor 模型 把"互斥"升级为消息队列,编译期即可检查"跨 actor 引用是否安全",让"数据竞争"成为编译错误。

Actor 101:30 秒速览

  1. 定义
swift 复制代码
actor Boss {
    var hp: Double = 100
    func takeDamage(_ amount: Double) {
        hp = max(0, hp - amount)
    }
}
  1. 调用规则
  • 内部:同步函数,直接访问 hp
  • 外部:必须通过 await 异步消息,编译器自动加队列。
swift 复制代码
let boss = Boss()
await boss.takeDamage(10)   // 编译通过
boss.hp                     // ❌ 编译错误:actor-isolated
  1. 关键保证

Actor 隔离域(isolation domain):同一时间只有一条消息在执行,天然"可线性化"(Serializability)。

把协议能力搬进 actor

目标:

  • 不破坏前两篇的泛型协议架构;
  • 让任何实体既能以"值语义"跑在单线程,也能以" actor 引用"跑在多线程;
  • 客户端/服务器共用同一套算法。
  1. 定义并发版协议
swift 复制代码
/// 可并发受伤
protocol ConcurrentWoundable: AnyObject {
    associatedtype Value: NumericValue
    func takeDamage(_ amount: Value) async
    var currentHp: Value { get }
}

注意:

  • AnyObject 限制只让 class/actor 符合,因为需要共享引用;
  • 方法标记 async,调用方必须 await
  1. 让 actor 直接符合
swift 复制代码
actor ConcurrentBoss<Value: NumericValue>: ConcurrentWoundable {
    private(set) var hp: Value
    let maxHp: Value
    
    init(hp: Value, maxHp: Value) {
        self.hp = hp; self.maxHp = maxHp
    }
    
    func takeDamage(_ amount: Value) async {
        hp = max(Value(0), hp - amount)
    }
    
    nonisolated var currentHp: Value { hp }   // 只读快照,无需 await
}

nonisolated 关键字:编译器允许外部同步读取,但不能写。

  1. 并发安全暴击算法

把上篇的 DamageCalculator 泛型算法保持值语义,计算过程无锁;只有最后 takeDamage 进 actor 才排队。

swift 复制代码
let calc = AnyDamageCalculator(Double.self) { base in base * 1.5 }
let damage = calc.calculate(base: 50)          // 无锁计算
await boss.takeDamage(damage)                  // 一次消息

分离"计算"与"状态变更":计算无锁、变更串行,兼顾性能与安全。

分布式 Actor:跨进程也能 "await boss.takeDamage"

Swift 5.9 起引入 distributed actor,同一语法即可跨进程/跨机器:

swift 复制代码
distributed actor RemoteBoss: ConcurrentWoundable {
    distributed func takeDamage(_ amount: Value) async {
        hp = max(Value(0), hp - amount)
    }
}

调用方:

swift 复制代码
let boss = try await RemoteBoss.resolve(id: bossID, using: .init())
await boss.takeDamage(30)

底层由 Swift gRPC 传输消息,开发者零成本获得分布式对象模型。

实战:并发 Boss 战模拟器

场景:

  • 4 个玩家并发放技能,伤害随机;
  • 1 个后台线程每 0.5 s 触发 dot;
  • 1 个渲染线程每帧读血量更新 UI;

代码:

swift 复制代码
protocol NumericValue: Comparable & Sendable {
    static func + (lhs: Self, rhs: Self) -> Self
    static func - (lhs: Self, rhs: Self) -> Self
    static func * (lhs: Self, rhs: Self) -> Self
    static func / (lhs: Self, rhs: Self) -> Self
    static func > (lhs: Self, rhs: Self) -> Bool   // 与标量乘
    init(_ value: Int)                               // 能从整数字面量初始化
}
extension Double: NumericValue {}

/// 可并发受伤
protocol ConcurrentWoundable: AnyObject {
    associatedtype Value: NumericValue
    func takeDamage(_ amount: Value) async
    var currentHp: Value { get }
}

// 1. 并发 BOSS
actor BossBattle: @preconcurrency ConcurrentWoundable {
    private(set) var hp: Double
    let maxHp: Double
    init(hp: Double) {
        self.hp = hp;
        self.maxHp = hp
    }
    
    func takeDamage(_ amount: Double) async {
        hp = max(0, hp - amount)
        if hp == 0 { print("BOSS 被击败!") }
    }
    
    var currentHp: Double { hp }
}

// 2. 玩家技能
func playerTask(id: Int, boss: BossBattle) async {
    for _ in 0..<5 {
        let damage = Double.random(in: 5...15)
        await boss.takeDamage(damage)
        print("Player\(id) 造成 \(damage)")
        try? await Task.sleep(for: .milliseconds(.random(in: 100...300)))
    }
}

// 3. dot 后台
func dotTask(boss: BossBattle) async {
    while await boss.currentHp > 0 {
        await boss.takeDamage(3)
        print("dot 3 点")
        try? await Task.sleep(for: .milliseconds(500))
    }
}

// 4. 渲染线程(只读)
func renderTask(boss: BossBattle) async {
    while await boss.currentHp > 0 {
        let hp = await boss.currentHp
        print("UI 血量:\(Int(hp))")
        try? await Task.sleep(for: .seconds(1/60))
    }
}

// 5. 启动

Task {
    let boss = BossBattle(hp: 100)
    let _ = await withDiscardingTaskGroup { group in
        for i in 1...4 {
            group.addTask {
                await playerTask(id: i, boss: boss)
            }
        }
        
        group.addTask {
            await dotTask(boss: boss)
        }
        
        group.addTask {
            await renderTask(boss: boss)
        }
    }
}

运行结果(节选):

erlang 复制代码
Player3 造成 11.0
Player1 造成 8.0
dot 3 点
UI 血量:78
...
BOSS 被击败!

全程无需手动加锁,编译器保证任何时刻只有一条消息在修改 hp

与 SwiftUI 无缝衔接

swift 复制代码
@MainActor
final class BossModel: ObservableObject {
    private let boss = BossBattle(hp: 100)
    
    @Published private(set) var hpText = ""
    
    func start() async {
        await renderLoop()
    }
    
    @MainActor
    private func renderLoop() async {
        while await boss.currentHp > 0 {
            hpText = "血量 \(Int(await boss.currentHp))"
            try? await Task.sleep(for: .seconds(1))
        }
        hpText = "BOSS 被击败"
    }
    
    func attack() async {
        await boss.takeDamage(Double.random(in: 10...20))
    }
}

@MainActor 保证所有 SwiftUI 状态更新跑在主线程;业务逻辑在后台 actor 串行执行,零数据竞争。

常见坑与最佳实践

  1. 在 actor 里访问全局可变状态

    同样要 await,否则编译报错。

  2. nonisolated 只能读,不能写;写必须走消息。

  3. 不要把长时间阻塞代码(sleep、sync I/O)直接放进 actor,会卡住消息队列;应拆到 Task.detachedAsyncSequence

  4. 跨 actor 调用时,值类型会被拷贝,不要传递大型数组;可改用 AsyncSequence 流式输出。

  5. 分布式 actor 的方法参数/返回值必须遵循 Codable,否则无法序列化

相关推荐
HarderCoder2 小时前
Swift 一个小型游戏对象模型渐进式设计(三)——把能力再抽象一层,写一套“伤害计算器”框架
swift
HarderCoder2 小时前
Swift 一个小型游戏对象模型渐进式设计(二)——协议与默认实现:如何写出不用继承的多态
swift
HarderCoder3 小时前
Swift 一个小型游戏对象模型渐进式设计(一)——继承机制解读:从基础类到防止重写
swift
HarderCoder4 小时前
Swift 中的迭代机制:Sequence、Collection 与 Iterator 完全拆解
swift
HarderCoder9 小时前
告别并发警告:Swift 6 线程安全通知 MainActorMessage & AsyncMessage 实战指南
swift
HarderCoder9 小时前
【SwiftUI 任务身份】task(id:) 如何正确响应依赖变化
swift
非专业程序员10 小时前
精读GitHub - swift-markdown-ui
ios·swiftui·swift
5***79001 天前
Swift进阶
开发语言·ios·swift
大炮走火1 天前
iOS在制作framework时,oc与swift混编的流程及坑点!
开发语言·ios·swift