如何让Actor遵循要求非隔离方法的协议并安全修改内部状态?
如何让Actor遵循要求非隔离方法的协议并安全修改内部状态?
这问题我太熟了!刚好之前在处理AVFoundation代理(比如你提到的AVAssetWriterDelegate)时踩过一模一样的坑,尤其是在SWIFT_STRICT_CONCURRENCY=Complete的严格模式下,编译器的检查简直是寸步不让。咱们一步步来解决这个问题:
核心问题拆解
你遇到的矛盾点很明确:
- 协议(比如AVAssetWriterDelegate)要求实现非隔离方法,但Actor的内部状态只能在隔离上下文中修改
- 自己用DispatchQueue同步的写法,编译器不认,因为它不知道你自定义的队列和Actor的隔离上下文是绑定的
- 换成final class又会触发Sendable的可变属性警告
最安全、最符合Swift并发模型的方案还是用Actor,咱们只需要用编译器认可的方式切换到隔离上下文就行。
方案一:Actor + 隔离内部方法 + Task切换上下文
这是最通用的解法,不管协议方法在哪个队列上调用都能用,编译器能完全验证安全性。
思路很简单:
- 给Actor加一个私有隔离方法,专门负责修改内部状态(编译器会确保这个方法只能在Actor的串行执行器上运行)
- 在非隔离的协议方法里,用
Task把操作调度到Actor的隔离上下文里
代码示例:
import Foundation // 模拟你无法修改的P协议,对应真实场景的AVAssetWriterDelegate protocol P: NSObjectProtocol, Sendable { func mutateState() } actor A: NSObject, P { var number: Int = 0 // 隔离的内部方法,负责安全修改状态 private func incrementNumber() { number += 1 } nonisolated func mutateState() { // 用Task切换到Actor的隔离上下文,避免数据竞争 // 加[weak self]是为了防止循环引用,NSObject+Actor的组合特别要注意这个 Task { [weak self] in await self?.incrementNumber() } } }
为什么编译器会认可?
incrementNumber是Actor的隔离方法,编译器会强制它只能在Actor自己的串行执行器上运行,从根源避免数据竞争Task会自动把闭包调度到Actor的执行器上,await会确保操作在正确的上下文完成(代理场景下异步执行就足够了)
方案二:把Actor执行器绑定到代理队列(针对AVAssetWriterDelegate场景优化)
对于AVAssetWriterDelegate这类协议,你在初始化AVAssetWriter时会指定一个串行队列,所有代理方法都会固定在这个队列上调用。这时候咱们可以把Actor的执行器直接绑定到这个队列,这样代理方法运行时就相当于在Actor的隔离上下文中,效率更高。
代码示例:
import AVFoundation actor AssetWriterDelegate: NSObject, AVAssetWriterDelegate { private let writerQueue: DispatchQueue // 把Actor的执行器绑定到AVAssetWriter的代理队列 nonisolated var unownedExecutor: UnownedSerialExecutor { writerQueue.asUnownedSerialExecutor() } var number: Int = 0 init(writerQueue: DispatchQueue) { self.writerQueue = writerQueue super.init() } // AVAssetWriterDelegate的非隔离代理方法 nonisolated func assetWriter(_ writer: AVAssetWriter, didOutputSegmentData segmentData: Data, segmentType: AVAssetSegmentType, segmentReport: AVAssetSegmentReport?) { // 因为当前方法在writerQueue上运行,而writerQueue是Actor的执行器 // 用Task切换到隔离上下文调用处理方法,编译器完全认可 Task { [weak self] in await self?.processSegment(segmentData) } } // 隔离的内部处理方法,安全修改状态 private func processSegment(_ data: Data) { number += 1 // 这里可以安全处理任何Actor内部状态 } } // 使用方式: let writerQueue = DispatchQueue(label: "com.example.assetwriter.queue") let delegate = AssetWriterDelegate(writerQueue: writerQueue) let assetWriter = AVAssetWriter(url: URL(fileURLWithPath: "test.mp4"), fileType: .mp4)! assetWriter.delegate = delegate assetWriter.startWriting()
这种方法的好处是没有额外的调度开销,代理事件直接在指定队列上处理,同时完全符合Actor的并发安全要求。
方案三:万不得已用final class的兼容写法
如果因为某些限制必须用final class,那咱们得手动保证可变属性的线程安全,同时满足Sendable要求:
写法1:用串行队列保护可变属性
final class A: NSObject, P, Sendable { // 用串行队列同步所有属性读写 private let syncQueue = DispatchQueue(label: "com.example.sync.queue") private var _number: Int = 0 // 对外暴露的属性,读写都通过队列同步 var number: Int { get { syncQueue.sync { _number } } set { syncQueue.sync { _number = newValue } } } nonisolated func mutateState() { number += 1 } }
写法2:用@MainActor标记(适合所有操作都在主线程的场景)
@MainActor final class A: NSObject, P, Sendable { var number: Int = 0 nonisolated func mutateState() { Task { number += 1 } } }
⚠️ 注意:这种方法需要你自己保证所有属性访问都通过同步机制,编译器无法自动验证,不如Actor方案安全,除非万不得已不推荐。
避坑提醒
- 别自己用DispatchQueue直接访问Actor属性:哪怕队列是串行的,编译器不知道这个队列和Actor的隔离上下文的关系,肯定会报错。只有Actor自己的执行器或者通过Task调度,编译器才会认可。
- 严格并发模式下别耍小聪明:
SWIFT_STRICT_CONCURRENCY=Complete就是要编译器完全验证所有并发安全,所以必须用编译器认可的写法,不能依赖“实际安全但编译器不知道”的逻辑。 - AVFoundation代理的特殊点:一定要把Actor的执行器绑定到你给AVAssetWriter指定的代理队列,这是最优解,既符合并发模型又高效。
内容来源于stack exchange
相关产品推荐
相关产品推荐

