I have a class type I don't own that I would like to access serially so as to ensure there can only be one of the effects it performs in flight at once. I believe I could wrap the type in an actor, and expose endpoints as needed.
actor MyActor {
let fooClassDependency: FooClassDependency
func endpoint() async throws {
try await withCheckedThrowingContinuation { continuation in
// some effect
self.fooClassDependency.bar()
// ...
}
}
}
But I wonder if I could achieve the same end more simply by extending the type with a MainActor-annotated endpoint:
Is my theorizing correct that I would be guaranteed calls to extensionEndpoint() would execute serially?
If I'm wrong, is there another way to achieve this end in a lighter-weight way than wrapping the dependency?
Secondarily, I don't actually need this effect to execute on the main actor, I'm just using it because I know the annotation exists. Are there other stock actor annotations, eg one equivalent to DispatchQueue.global(qos: .userInteractive) or the like?
I do believe that in both cases, calls to extensionEndpoint() would indeed be serialised.
However, I do not believe you need to wrap your calls to fooClassDependency.bar() in a withCheckedThrowingContinuation scope -- if bar() itself is not async, it will be run within the context of the underlying actor executor, as there exists no suspension point where the actor can yield and/or jump to another executor.
I also do not believe there exists other global actor out-of-the-box other than @MainActor, but you may create your own: Modern Concurrency in Swift, Chapter 9: Global Actors | raywenderlich.com
Not sure this is recommended in your use case though, as it will restrict concurrency between unrelated tasks.
I didn't bother to elaborate the particulars of the dependency's effect in the original post, but it is indeed a callback-based API, and I want to expose it as an async endpoint, hence the continuation. I just wanted to make clear the reason I am making my endpoint independently asynchronous, regardless of being wrapped in an actor or not.
Actor is reentrant. This means that func endpoint() async -> Result can be called several times. Yes, it is guaranteed that at the same time only one task can be executed on an actor. But after function is suspended, another call can occur.
Image the following:
func endpoint() async -> Result is called. Under the hood it calls the withCheckedThrowingContinuation function, where old callback based function is called. Immediately after that withCheckedThrowingContinuation become suspended until continuation will be resumed. So, as it becomes suspended, actor "become free" and can execute another func.
func endpoint() async -> Result is called one more time, and then one more....
lots of call to func endpoint() async -> Result were made
the first call of func endpoint() async -> Result resumed