You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让Actor遵循要求非隔离方法的协议并安全修改内部状态?

如何让Actor遵循要求非隔离方法的协议并安全修改内部状态?

这问题我太熟了!刚好之前在处理AVFoundation代理(比如你提到的AVAssetWriterDelegate)时踩过一模一样的坑,尤其是在SWIFT_STRICT_CONCURRENCY=Complete的严格模式下,编译器的检查简直是寸步不让。咱们一步步来解决这个问题:

核心问题拆解

你遇到的矛盾点很明确:

  • 协议(比如AVAssetWriterDelegate)要求实现非隔离方法,但Actor的内部状态只能在隔离上下文中修改
  • 自己用DispatchQueue同步的写法,编译器不认,因为它不知道你自定义的队列和Actor的隔离上下文是绑定的
  • 换成final class又会触发Sendable的可变属性警告

最安全、最符合Swift并发模型的方案还是用Actor,咱们只需要用编译器认可的方式切换到隔离上下文就行。


方案一:Actor + 隔离内部方法 + Task切换上下文

这是最通用的解法,不管协议方法在哪个队列上调用都能用,编译器能完全验证安全性。

思路很简单:

  1. 给Actor加一个私有隔离方法,专门负责修改内部状态(编译器会确保这个方法只能在Actor的串行执行器上运行)
  2. 在非隔离的协议方法里,用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方案安全,除非万不得已不推荐。


避坑提醒

  1. 别自己用DispatchQueue直接访问Actor属性:哪怕队列是串行的,编译器不知道这个队列和Actor的隔离上下文的关系,肯定会报错。只有Actor自己的执行器或者通过Task调度,编译器才会认可。
  2. 严格并发模式下别耍小聪明:SWIFT_STRICT_CONCURRENCY=Complete就是要编译器完全验证所有并发安全,所以必须用编译器认可的写法,不能依赖“实际安全但编译器不知道”的逻辑。
  3. AVFoundation代理的特殊点:一定要把Actor的执行器绑定到你给AVAssetWriter指定的代理队列,这是最优解,既符合并发模型又高效。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 13:23:05