使用DispatchSerialQueue作为Actor自定义Executor时assumeIsolated调用失败
assumeIsolated触发运行时错误 我想实现一个Actor,用来适配NWNetwork这类需要指定Dispatch队列和回调的“遗留”框架。为了避免在回调里通过Task调度到隔离上下文,我让整个Actor运行在自定义的DispatchSerialQueue上,这个队列同时也是遗留框架使用的队列。
我把回调写成了@Sendable且nonisolated的成员函数,确保回调会在指定队列上调用,但调用self.assumeIsolated(_:)时触发了运行时错误:
Fatal error: Incorrect actor executor assumption; Expected same executor as MyLib.TestActor.
最小测试用例里,await actor.isolatedFunc()能正常运行,但模拟遗留框架调用回调时就报错,堆栈跟踪显示当前确实在指定队列上。我认为DispatchSerialQueue应该是合规的SerialExecutor,断言应该通过,求问题原因和解决办法。
测试Actor代码
public final actor TestActor { let queue = DispatchSerialQueue(label: "test_actor_queue") nonisolated public var unownedExecutor: UnownedSerialExecutor { queue.asUnownedSerialExecutor() } func isolatedFunc() { self.preconditionIsolated() } @Sendable nonisolated func nonIsolatedFunc() { self.queue.async { self.callback() } } @Sendable nonisolated func callback() { dispatchPrecondition(condition: .onQueue(queue)) self.assumeIsolated { this in // <== 运行时错误 this.isolatedFunc() } } }
XCTest测试代码
final class HTTPServerTests: XCTestCase { func testExample() async throws { let actor = TestActor() await actor.isolatedFunc() actor.nonIsolatedFunc() } }
堆栈跟踪
Thread 4 Queue : test_actor_queue (serial) #0 0x000000019326df68 in _swift_runtime_on_report () #1 0x000000019330614c in _swift_stdlib_reportFatalErrorInFile () #2 0x0000000192fcb2d8 in _assertionFailure(_:_:file:line:flags:) () #3 0x00000002099cb298 in Actor.assumeIsolated<τ_0_0>(_:file:line:) () #4 0x00000001053a2918 in TestActor.callback@Sendable () #5 0x00000001053a27a0 in closure #1 in TestActor.nonIsolatedFunc@Sendable () #6 0x00000001053a2978 in thunk for @escaping @callee_guaranteed @Sendable () -> () ()
原因分析
Swift Actor的assumeIsolated不仅会检查当前是否在指定的串行队列上,还会验证当前执行上下文的Executor身份是否和Actor的unownedExecutor完全匹配。
这里的核心问题是:DispatchSerialQueue.asUnownedSerialExecutor()每次调用都会返回一个新的UnownedSerialExecutor实例,而Actor的unownedExecutor属性每次访问也会创建新实例。当你在回调里调用assumeIsolated时,系统会拿当前队列的Executor实例和Actor的unownedExecutor返回的实例做身份比较——因为是两个不同的实例,所以断言失败。
解决办法
把queue.asUnownedSerialExecutor()的结果缓存起来,让Actor的unownedExecutor始终返回同一个实例:
修改后的TestActor代码:
public final actor TestActor { let queue = DispatchSerialQueue(label: "test_actor_queue") // 缓存Executor实例,确保身份唯一 private let executor: UnownedSerialExecutor nonisolated public var unownedExecutor: UnownedSerialExecutor { executor } init() { executor = queue.asUnownedSerialExecutor() } func isolatedFunc() { self.preconditionIsolated() } @Sendable nonisolated func nonIsolatedFunc() { self.queue.async { self.callback() } } @Sendable nonisolated func callback() { dispatchPrecondition(condition: .onQueue(queue)) self.assumeIsolated { this in this.isolatedFunc() } } }
补充说明
- 当你通过
await actor.isolatedFunc()调用时,Swift会自动使用Actor的unownedExecutor来调度任务,此时Executor实例是一致的,所以不会报错。 - 缓存Executor实例后,无论多少次访问
unownedExecutor,返回的都是同一个对象,和当前队列的Executor实例身份完全匹配,assumeIsolated的断言就会通过。
内容的提问来源于stack exchange,提问作者CouchDeveloper

