主Actor隔离对象deinit中资源释放的Swift并发安全问题
我有一个标记了@MainActor的TimerScheduler类,核心代码如下:
@MainActor open class TimerScheduler { private enum TimerState { case normal case paused(remaining: TimeInterval) } private struct TimerInfo { let timer: Timer let state: TimerState } private var keyToTimerInfoMap = [String:TimerInfo]() private let lifecycleChecker: () -> Bool public init(lifecycleChecker: @escaping @MainActor () -> Bool = { true }) { self.lifecycleChecker = lifecycleChecker NotificationCenter.default.addObserver(self, selector: #selector(pause), name:UIApplication.willResignActiveNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(resume), name:UIApplication.didBecomeActiveNotification, object: nil) } deinit { NotificationCenter.default.removeObserver(self) for (_, info) in keyToTimerInfoMap { info.timer.invalidate() } } private func unschedule(key: String) { guard let info = keyToTimerInfoMap[key] else { return } info.timer.invalidate() keyToTimerInfoMap.removeValue(forKey: key) } // 其他函数 }
编译时触发警告:
Cannot access property 'keyToTimerInfoMap' with a non-sendable type '[String : TimerScheduler.TimerInfo]' from non-isolated deinit; this is an error in the Swift 6 language mode
原因很明确:deinit可能在任意线程执行,而keyToTimerInfoMap是@MainActor隔离的非Sendable属性,跨线程访问违反并发安全规则。Xcode提示让TimerInfo遵循Sendable,但Timer本身不支持Sendable,这条路走不通。
我尝试过用@MainActor Task包裹deinit里的资源释放逻辑:
deinit { NotificationCenter.default.removeObserver(self) Task { @MainActor in for (_, info) in keyToTimerInfoMap { info.timer.invalidate() } } }
但担心如果对象在后台线程执行deinit,此时访问keyToTimerInfoMap可能已经被释放,存在野指针风险。当前环境为Xcode 16 beta 3、Swift 5 + 完整并发检查模式。
可行解决方案
方案1:给deinit标记@MainActor(推荐)
既然整个类是@MainActor隔离的,我们可以强制deinit在主线程执行,这样就能安全访问@MainActor绑定的属性:
@MainActor deinit { NotificationCenter.default.removeObserver(self) for (_, info) in keyToTimerInfoMap { info.timer.invalidate() } }
这种方式完全符合类的设计意图——@MainActor类的实例本就应该尽量在主线程释放,既消除了并发警告,也避免了Task延迟执行带来的对象生命周期问题。
方案2:捕获map副本后在Task中处理
如果无法强制deinit在主线程执行,可先在当前线程捕获keyToTimerInfoMap的副本,再在@MainActor Task中处理:
deinit { NotificationCenter.default.removeObserver(self) // 先捕获副本,避免后续访问已释放的原对象 let timerInfos = Array(keyToTimerInfoMap.values) Task { @MainActor in for info in timerInfos { info.timer.invalidate() } } }
注意:由于类是@MainActor隔离的,所有Timer实例都是在主线程创建的,在主线程调用invalidate是安全的。捕获副本后,即使原对象被释放,副本中的Timer引用仍然有效,可正常执行销毁逻辑。
补充说明
Timer不支持Sendable的原因:它是NSObject子类,内部状态并非线程安全,Swift默认不会将其标记为Sendable。强行给TimerInfo添加@unchecked Sendable会隐藏真实的并发风险,不建议这么做。- 方案1是最优解,既简洁又符合并发安全规范,无需额外的副本创建和Task调度开销。
内容的提问来源于stack exchange,提问作者OMGPOP

