问题:Actor 不是「一进到底」
Swift 的 Actor 能保证同一时刻只有一个任务在隔离域里执行 ,但在 await 挂起 时,Actor 会释放执行权,其他发往该 Actor 的调用可以继续执行。这就是常见的 Actor 重入(reentrancy):你以为「上一条逻辑还没跑完」,实际上中间已经插入了别的消息处理。
典型后果包括:
- 生命周期或状态机类 API(初始化 → 运行 → 销毁)被交错执行;
- 共享资源在「以为仍独占」时被另一条路径修改;
- 调试时表现为顺序与代码书写顺序不一致。
若业务语义要求「前一次异步流程整段结束 (包含其内部所有 await)后,再开始下一次」,仅靠 Actor 的默认调度是不够的,需要显式串行化。
重入:用打印顺序看清「中间插了一刀」
下面这个 Actor 有两个异步方法,内部都有一次 await Task.yield()(可换成任何真正的异步点):
swift
import Foundation
actor InterleavingDemo {
func taskA() async {
print("A: step 1")
await Task.yield()
print("A: step 2")
}
func taskB() async {
print("B: only step")
}
}
从外部几乎同时 发起 taskA 和 taskB:
swift
let demo = InterleavingDemo()
await withTaskGroup(of: Void.self) { group in
group.addTask { await demo.taskA() }
group.addTask { await demo.taskB() }
}
可能出现的输出之一(重入):
text
A: step 1
B: only step
A: step 2
含义很具体:taskA 在第一个 await 挂起后,taskB 整段插进来跑完 ,然后 taskA 才继续。若这里维护的是「会话 / 引擎生命周期」或「依赖连续不变量的状态机」,这种交错往往就是 bug 来源。
思路:用队尾屏障把异步工作连成 FIFO 链
串行异步门闩 (AASerialAsyncGate)的核心思想是:
- 维护一个
tailBarrier:表示「当前队列里,排在队尾之前的那条链何时算全部结束」; - 每次
run时,新任务必须先await前一个屏障 ,再执行自己的operation; - 执行完毕后更新屏障,让后续任务继续排队。
这样,同一时刻逻辑上只有一条链在执行 ,新调用不会与前序调用的 await 间隙「插队」到业务语义的前面。
AASerialAsyncGate 实现(源码)
swift
//
// AASerialAsyncGate.swift
//
// 串行异步任务队列:每次 `run` 将闭包入队到队尾;新任务会等待此前入队的全部任务
// 整段完成(含其内部所有 await)后才开始执行,执行完毕自动出队(屏障前移)。
// 从而避免宿主 Swift Actor 在 await 处重入时,多条生命周期调用交错执行。
//
import Foundation
public final class AASerialAsyncGate: @unchecked Sendable {
/// 队尾屏障:完成即表示当前队列中此前所有任务均已结束;新任务必须先 `await` 再执行自身逻辑。
private var tailBarrier: Task<Void, Never> = Task {}
public init() {}
/// 将 `operation` 入队;前序任务全部结束后才执行;同一时刻逻辑上仅一条链在执行。
/// 统一为 `async throws`:`Task.value` 的 Failure 与 `rethrows` 不兼容,故不用 `rethrows`。
/// 闭包本身不抛错时,宿主侧可用 `try? await gate.run { ... }`。
public func run<T: Sendable>(
_ operation: @escaping @Sendable () async throws -> T
) async throws -> T {
let work: Task<T, Error>
let predecessor = tailBarrier
work = Task {
await predecessor.value
return try await operation()
}
tailBarrier = Task {
_ = try? await work.value
}
return try await work.value
}
}
要点:后来的 run 必须先等「前一个 tailBarrier 代表的整段 operation(含内部所有 await)结束」 ,因此同一 gate 上的多段逻辑在时间上不会 在彼此的 await 缝隙里交错。
为何统一为 async throws
Task.value 的 Failure 与 rethrows 在类型系统上不易直接对齐,故统一为 async throws;若闭包本身不抛错,调用侧可用 try? await gate.run { ... }。
验证示例:对比「仅 Actor」与「Actor + Gate」
1. 仅 Actor:仍可能出现交错
swift
actor EngineWithoutGate {
private let name: String
init(name: String) { self.name = name }
func work(_ label: String) async {
print("[\(name)] \(label) --- before await")
await Task.yield()
print("[\(name)] \(label) --- after await")
}
}
func demoActorOnly() async {
let engine = EngineWithoutGate(name: "E1")
await withTaskGroup(of: Void.self) { group in
group.addTask { await engine.work("call-1") }
group.addTask { await engine.work("call-2") }
}
}
多次运行或依赖调度,有机会看到 call-2 的整段插在 call-1 的 before/after 之间,这就是要防的重入现象。
2. 加上 AASerialAsyncGate:同一 gate 上严格 FIFO
swift
actor EngineWithGate {
private let name: String
private let gate = AASerialAsyncGate()
init(name: String) { self.name = name }
func work(_ label: String) async {
try? await gate.run {
print("[\(name)] \(label) --- before await")
await Task.yield()
print("[\(name)] \(label) --- after await")
}
}
}
func demoWithGate() async {
let engine = EngineWithGate(name: "E2")
await withTaskGroup(of: Void.self) { group in
group.addTask { await engine.work("call-1") }
group.addTask { await engine.work("call-2") }
}
}
稳定期望 (两段 work 都经同一 gate 排队时):先完整跑完 call-1(before → after),再跑 call-2(before → after),例如:
text
[E2] call-1 --- before await
[E2] call-1 --- after await
[E2] call-2 --- before await
[E2] call-2 --- after await
可将 print 换成收集到 [String] 的回调,在 XCTest 中断言顺序,作为自动化验证。
3. 宿主是 Actor 时的典型用法
Actor 仍会在自己的方法之间重入;门闩只保证「放进 gate.run 里的那几段」彼此不穿插 。生命周期 API 可全部包在 lifecycleGate.run 里:
swift
actor Service {
private let lifecycleGate = AASerialAsyncGate()
func startSession() async {
try? await lifecycleGate.run {
await connect()
await configure()
}
}
func stopSession() async {
try? await lifecycleGate.run {
await teardown()
}
}
private func connect() async { await Task.yield() }
private func configure() async { await Task.yield() }
private func teardown() async { await Task.yield() }
}
小结
| 场景 | 说明 |
|---|---|
| 重入例子 | 同一 Actor 上两个 async 方法并发调用时,await 后可能打印出 A1 → B → A2。 |
| 门闩行为 | AASerialAsyncGate 用 tailBarrier 链式 Task,保证后一次 run 等前一次整段结束。 |
| 代价 | 额外 Task 调度与内存;只适合「必须严格串行」的路径。 |
工程内若存在与 AASerialAsyncGate 等价的类型,可直接对照实现。