为何不能将Actor用作AsyncSequence中的状态机类型?
笔记内容
以下是WWDC 2023会话Beyond the basics of structured concurrency的笔记内容:
许多AsyncSequence是通过状态机实现的,我们用状态机来停止运行中的序列。
示例代码:
public func next() async -> Order? { return await withTaskCancellationHandler { let result = await kitchen.generateOrder() guard state.isRunning else { return nil } return result } onCancel: { state.cancel() } }
笔记要点:
- 尽管Actor非常适合保护封装状态,但它无法真正保护状态机。
- 对于修改和读取状态机的单个属性,Actor并非合适的工具。
- 无法保证Actor上操作的运行顺序,因此无法确保取消操作优先执行。
- 建议改用Swift Atomics包中的原子操作、dispatch队列或锁。
推荐的实现方式:
private final class OrderState: Sendable { let protectedIsRunning = ManagedAtomic<Bool>(true) var isRunning: Bool { get { protectedIsRunning.load(ordering: .acquiring) } set { protectedIsRunning.store(newValue, ordering: .relaxed) } } func cancel() { isRunning = false } }
核心疑问解答
为什么Actor在这里不是合适的选择?
Actor的核心逻辑是通过串行化消息队列保证状态安全,所有对Actor状态的访问、修改请求都要排队执行。但在这个AsyncSequence的取消场景里,我们的需求是取消操作能立即生效,且后续状态检查能立刻感知到取消结果,Actor的特性刚好和这个需求冲突:
操作优先级无法保障:当
kitchen.generateOrder()正在执行时触发取消(调用state.cancel()),如果state是Actor,这个取消请求会被放到Actor的消息队列末尾。而generateOrder()完成后,读取state.isRunning的请求也会进入队列,极有可能出现「先执行状态读取、再执行取消操作」的顺序——这就导致我们无法及时检测到取消,依然会返回任务结果,完全违背了取消的意图。不必要的调度开销:Actor的每一次状态访问都需要经过异步调度,对于
isRunning这种简单布尔值的读写来说,这种开销是冗余的。原子操作、锁或者串行队列能以更低的成本完成状态同步,更适配这种轻量状态机的场景。状态即时性需求不匹配:状态机的状态变化需要即时可见,Actor的异步特性会让状态的读取、修改存在延迟,无法满足状态机对状态一致性的即时性要求。而原子操作可以保证内存访问的顺序性和可见性,确保取消操作执行后,后续的状态读取能立刻获取到最新值。
内容的提问来源于stack exchange,提问作者Alex

