Calling an actor method in a sync context: which method is better?

assuming the following:

  1. the MyActor API looks something like this:
@globalActor
actor MyActor {
    static let shared = MyActor()

    func anotherSyncFunc() {}
}
  1. there's no configuration that causes syncFunc() to be inferred as actor-isolated

then, to address these in reverse order:

the await is required because the compiler does not currently piece together the fact that @MyActor and calls to actor-isolated methods of the instance returned by MyActor.shared have the same static isolation, so it thinks there is an isolation crossing (hence possible suspension point). if you mark the anotherSyncFunc() call as @MyActor, then you will no longer be required to await it from contexts that are statically known to have the same isolation. granted, if you actually have any state the actor operates on, that will also need to be marked with the same global actor to avoid the same issue within the actor methods themselves (which perhaps means you're effectively dealing with global state isolated to the singleton actor instance, which can be modeled in various ways).

using the global actor annotation in the closure's signature can improve the amount of static reasoning the compiler is able to do, which may lead to fewer annotations being needed (e.g. as in the previous case), and changes actor inference propagation in some instances. additionally, its use in this scenario means that Task.init can synchronously enqueue the work directly on MyActor.shared's executor, without first having to potentially 'hop off' an existing actor's executor to do so. this means it may end up being a bit more efficient at runtime, but perhaps more importantly, it also means you won't lose certain ordering guarantees about when the closure will actually be scheduled relative to other things – after the Task.init returns, the global-actor-isolated work will be enqueued.

2 Likes