Calling a MainActor function when already on main thread without a dispatch

I have a function (func A) annotated as MainActor that updates some state. In another function (func B) that is not MainActor, and it's also not async, I need to query a dependency for some state and potentially update my state which needs to be done on the MainActor. If the user had called func B on the main thread, I would like to not have to dispatch to the main queue in order to call func A. The user would expect that the state would be correct after calling func B.

Essentially, what I would like to do is the equivalent of something like this:

// func B is synchronous and non main actor
func B() {
    var someState = dependency.query()
    
    // func A is @MainActor
    if Thread.isMainThread {
        // This call to A does not compile
        self.A(someState)
    } else {
        DispatchQueue.main.async {
            // This call to A compiles
            self.A(someState)
        }
    }
}

Is there a way to achieve this?

1 Like

You probably don't want to mix actors, threads, and queues. You should simply be able to call a @MainActor function and have it do the right thing, unless I'm misunderstanding the question. The compiler will enforce inserting await as needed.

Where the above code is pasted is not in an async context, so I can't do an await. I will try to update the question to make that more clear.

Ah I see. This boils down to the function coloring problem then. I've somewhat lost track of the current state of the art on that question, so I'll defer to someone else.

No. To do that, B would have to be both synchronous and asynchronous at the same time.

Since A is isolated to an actor, self.A(someState) has a suspension point. If some other access to the actor is already pending when B gets to this point, B has no way of waiting across the suspension.

So, B has to be asynchronous. Chances are, if you allow asynchronicity flow "upstream" from B to callers of B and so on, you'll find a natural place to transition from synchronous to asynchronous (probably by using something like Task { … }.

Thanks for the reply.

I see what you are saying. Unfortunately in this case function B is actually the initializer. I could make that async, or I could make it MainActor. However, we didn't really want to have to do either for this particular class.

Has anyone figured out a way to do this? This is necessary to bridge to backwards compatible types. Here's an example of my use case:

// Backwards compatabile interface for pre-iOS 15
protocol ControllerProtocol {
    var isValue: Bool { get }
}

// iOS 15+ Implementation
@available(iOS 15.0, *)
final class Controller: ControllerProtocol {
    var isValue: Bool {
        if Thread.isMainThread {
            return isValue
        } else {
            return DispatchQueue.main.sync {
                return isValue
            }
        }
    }
            
    @MainActor
    var isValue: Bool = true
}

There are a couple of problems. First, the protocol can't be satisfied by the version annotated as @MainActor, so it's arguable the whole premise fails. Secondly, you can't access isValue in its own getter in the older version, so you might need to be shadowing the real version in a computed property.

Ignoring all that, your older code embeds the situation where it could wait; why wouldn't you therefore do let foo = await isValue in the newer version anywhere you need access?

There was a typo in my sample code, the actor isolated var should have another name (e.g. newIsValue) and the computed property should be using that for storage. The computed property is meant to satisfy the protocol, not the actor isolated version. On newer OS's you absolutely could await the value, but the point of my example is to offer backwards compatibility where you can't await. Alternatively you could be in a synchronous context where you also can't await.