iosswiftconcurrency

Is code running on main thread guaranteed to be safe to `MainActor.assumeIsolated`


I have the following code and I am not sure whether it's safe:

extension Timer {

  @MainActor // <- 1
  static func myScheduled(
    interval: TimeInterval, 
    block: @escaping @MainActor @Sendable () -> Void) -> Timer // <- 2
  {
    
    return scheduledTimer(withTimeInterval: interval, repeats: true) { _ in
      MainActor.assumeIsolated(block) // <- 3
    }
  }
}

In the above code, the first @MainActor annotation (mark 1 in code) guarantees that the myScheduled function must be called from main actor isolated context, which means that Timer.scheduledTimer will put the timer on RunLoop.main, which means that the timer callback must be called on main thread.

My block needs to call some API on the main actor, so I have to mark it as main actor (mark 2 in code). However, since the compiler doesn't know that the timer callback will run on main thread, I have to use MainActor.assumeIsolated(block) (mark 3 in code) here, to silence the compiler warning.

My question is, is it always safe to make this assumption? I know for sure that the timer callback happens on main thread, however, I am not sure whether it is always safe to assume a main thread context must be main isolated.

From the API doc of MainActor.assumeIsolated:

This method allows to assume and verify that the currently executing synchronous function is actually executing on the serial executor of the MainActor.

This check is performed against the MainActor’s serial executor, meaning that / if another actor uses the same serial executor–by using sharedUnownedExecutor as its own unownedExecutor–this check will succeed , as from a concurrency safety perspective, the serial executor guarantees mutual exclusion of those two actors.

In other words, if something runs on main thread, are we sure that it must be executed by this main actor's "executor"?

In case it's helpful, main thread != main queue != main actor. For example, from the discussion here: iOS async completion block not called, we know that viewDidLoad is on main thread, but not on main queue. Also UIViewController is already marked as main actor, which means viewDidLoad is on main thread + main actor isolated, but not on main queue.


Solution

  • In SE-0424, Apple tells us:

    Being able to assert isolation for non-task code this way is important enough that the Swift runtime actually already has a special case for it: even if the current thread is not running a task, isolation checking [such as MainActor.assumeIsolated] will succeed if the target actor is the MainActor and the current thread is the main thread.

    The Migrate your app to Swift 6 video shows example of this. In that example, it is some random delegate method that is getting called on the main thread, but the same is true here. In short, if you have some legacy API on the main thread, you can use this MainActor.assumeIsolated pattern.

    When using MainActor.assumeIsolated (introduced to Swift 5.9 in SE-0392), it is the developer who is vouching that it is on the main thread, and the executable will perform runtime checks rather than enjoying static data-race safety. But in those cases where the API has not migrated to Swift concurrency (as contemplated in SE-0337) but you know with certainty that it is running on the main thread, this is a short-term workaround.


    FWIW, SE-0431, SE-0414, and SE-0423 provide additional discussions regarding assumeIsolated, but admittedly are not directly related to the question at hand.