为什么"并发"突然成了刚需
真实场景里:
- 游戏服务器: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 秒速览
- 定义
swift
actor Boss {
var hp: Double = 100
func takeDamage(_ amount: Double) {
hp = max(0, hp - amount)
}
}
- 调用规则
- 内部:同步函数,直接访问
hp; - 外部:必须通过
await异步消息,编译器自动加队列。
swift
let boss = Boss()
await boss.takeDamage(10) // 编译通过
boss.hp // ❌ 编译错误:actor-isolated
- 关键保证
Actor 隔离域(isolation domain):同一时间只有一条消息在执行,天然"可线性化"(Serializability)。
把协议能力搬进 actor
目标:
- 不破坏前两篇的泛型协议架构;
- 让任何实体既能以"值语义"跑在单线程,也能以" actor 引用"跑在多线程;
- 客户端/服务器共用同一套算法。
- 定义并发版协议
swift
/// 可并发受伤
protocol ConcurrentWoundable: AnyObject {
associatedtype Value: NumericValue
func takeDamage(_ amount: Value) async
var currentHp: Value { get }
}
注意:
AnyObject限制只让 class/actor 符合,因为需要共享引用;- 方法标记
async,调用方必须await。
- 让 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 关键字:编译器允许外部同步读取,但不能写。
- 并发安全暴击算法
把上篇的 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 串行执行,零数据竞争。
常见坑与最佳实践
-
在 actor 里访问全局可变状态
同样要
await,否则编译报错。 -
nonisolated只能读,不能写;写必须走消息。 -
不要把长时间阻塞代码(sleep、sync I/O)直接放进 actor,会卡住消息队列;应拆到
Task.detached或AsyncSequence。 -
跨 actor 调用时,值类型会被拷贝,不要传递大型数组;可改用
AsyncSequence流式输出。 -
分布式 actor 的方法参数/返回值必须遵循
Codable,否则无法序列化