OMGPOP
OMGPOP

Reputation: 952

How to deallocate resource in main actor isolated object in deinit

I have a main actor isolated class like this one:

@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
    
    // Note: When app is in background, NSTimer will only get a few minutes of execution.
    // Then the timer will be paused
    // These notifications are there to ensure NSTimer is paused immediately when app goes background.
    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)
  }

  // other functions

}

I have a warning:

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

This is reasonable because deinit can happen in any thread.

Xcode also suggests to convert TimerInfo Sendable, which I can't do because Timer is not Sendable.

I could wrap the deinit in a main actor Task like this:

  deinit {
    NotificationCenter.default.removeObserver(self)
    Task { @MainActor in
      for (_, info) in keyToTimerInfoMap {
        info.timer.invalidate()
      }
    }
  }

However, if the object is deinit'ed in the background thread, I would be using the object (accessing its keyToTimerInfoMap field) after it's been deallocated, which seems to be a dangerous thing to do.

I am using Xcode 16 beta 3, Swift 5 with "complete" concurrency checking.

Upvotes: 1

Views: 909

Answers (2)

Claus Jørgensen
Claus Jørgensen

Reputation: 26344

Long story short is that you can't. If you need something to be deallocated on the Main Actor you need to introduce a explicit @MainActor func stop() and call that before your object is deallocated by its owner (this can be necessary if you're wrapping things like the AVPlayer).

A better solution is to avoid having Main Actor isolated properties/objects that needs to be explicitly deallocated, like Silmaril suggested.

Consider not marking your entire class @MainActor as well, and only use it for the relevant (public) properties. There's really nothing in the code you shown us that requires use of MainActor.

Upvotes: 0

Alexey Matjuk
Alexey Matjuk

Reputation: 4301

Since keyToTimerInfoMap is a private property, which means, when deinit is executing it can't be mutated from other place (thread/actor). We should be able to safely make the cleanup.

And to disable static checking of data isolation we can use nonisolated(unsafe) property attribute introduced in Swift 5.10:

  nonisolated(unsafe)
  private var keyToTimerInfoMap = [String: TimerInfo]()

P.S. I'm not super expert in this topic. So, if I'm wrong - please don't hesitate to comment about what's wrong 😉

Upvotes: 1

Related Questions