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

主Actor隔离对象deinit中资源释放的Swift并发安全问题

解决@MainActor类deinit中访问非Sendable属性的并发警告问题

我有一个标记了@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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 14:13:10