The idea is that the future is only started once (while being put in the cache). When another call retrieves the future from the cache it is already running (or already finished).
I don't wan to create (further?) a digression, but how would the caller be notified of the result if it's not waiting for it?
Technically, the call isn't "blocking" in the sense that it's not blocking any thread. But it's still "blocking" in the sense that the current task can't continue until the method has returned.
Your model of actor appears to be equivalent to this:
runDetached { actor.method() }
or (more loosely) this:
actorQueue.async { actor.method() }
That's fine if you don't need to wait on any result. But if you want a result, then you're going to want to add a callback, and we get back to the problem async methods were meant to solve.
I‘m not sure I understand this example. This wouldn‘t work without any asynchronous code either, because it is just an infinite loop:
class InfiniteLoop {
func getValue() -> Int {
return computeValue()
}
private func computeValue() -> Int {
return getValue() + 1
}
}
It is certainly possible that systems of actors can deadlock but I have got the impression that with the proposed model it is easier to produce deadlocks and more difficult to write correct actor methods.
As far as I can see this boils down to this:
var task: Task.Handle<Void, Never>? = detach {
Task.sleep(1_000_000_000)
_ = await task!.getResult()
}
So an actor can participate in that, but it's not the actor per se that is causing the deadlock as I see it.
Never getting a reply because it was dropped or is being worked on until the end of the world is observably equivalent to "locking up" but it's not the same as as deadlocking, which specifically is "waiting on each-other" for the discussions at hand at least.
As for "are Swift actors... actors?" - sure they are. The message passing is pretty focused on request/reply rather than uni-directional messages which has it's ups and downs, and is caused single handedly by the focus on looking like familiar programming paradigms (functions) and async await to make the suspending efficient.
I would also suggest avoiding the phrase "blocking" to describe re-entrancy or not. Yeah one can think of it "as if blocking, but not really" but that only adds to the confusion. Sticking to words used by the proposal and runtime is more useful and productive, i.e. "A non-reentrant suspended actor, cannot process further work until the (non-reentrant) function it is running completes" is the complete and correct thing to say about those. Phrasing things in terms of blocking immediately causes confusion in terms of really blocking the thread which anything async/awaiting does not do. And yes, not accepting any new work until some "current work" has completed is absolutely the actor way of doing things; In no way is it counter to the async/await way of programming - as someone suggested upthread. Blocking is the one thing to not do with actors; suspending them absolutely is a thing.
The same deadlocking issues have been faced by various actor runtimes, starting from Erlang's (and Elixir's) gen_server, through Orleans Grain's request handlers etc. Akka is left out from this specific pain point as it fully embraces message passing (at the cost of very high verboseness when doing request/reply patterns).
For what it's worth I -- personally -- consider deadlocking much easier to understand, debug, built tools for, and work with than uncontrolled interleaving, and I strongly believe we need to implement reentrancy control in the runtime, as proposed in the future directions.
I sense a lot of confusion about these patterns and their implications from the latest replies; I had been planning to prepare a longer writeup with actor patterns and how they're expressible (or not directly expressible, but workarounds exist) in Swift – this just strengthens my conviction that such writeup is necessary and should be helpful. I can't right now jump onto it, but will do so shortly.
Fully agreed – I was just going to write something about how the proposal might need some patterns/examples so that we can form some best practices (similar to the property wrapper proposal, who's numerous examples were infinitely helpful in understanding them), and therefore make better-informed decisions about this proposal as a whole.
In the feedback by the core team in the first review, there seems no arguments around whether we want to use the name Actor or AnyActor as the type-erased protocol name. To me AnyActor aligns more consistently with Any and AnyObject, which preserves a unified naming pattern in Swift. Is there any reason/justification about why we still use Actor in the revised proposal?
I agree. As I see it all actor methods should return some sort of future or detached task handle (or nothing).
And that is actually sort of what is proposed here. Using async/await "callbacks". The only difference is that per default the caller postpones its remaining work until the actor method's "future" completes. The client may then use something like the previously named "async let" to not immediately wait on the "future" to complete, or even detach() to be completely independent of the actor responding.
That said, my mental picture of distributed actors would probably have method calls default to being independent of the "returned future", i.e. as if using detach { await actor.method() }. But I do not have any real experience with actors, so ![]()
What is your evaluation of the proposal?
-1. This version of the proposal has so restricted actor capabilities that it makes the feature much less useful while also increasing the difficulty of teaching and adopting it. Overall this proposal seems to have eschewed Swift's typically pragmatic nature in favor of one focused on purity, despite that purity having little benefit to Swift users. I've previously summarized many of these points, so I'll try to be brief.
- Removal of subtyping greatly limits the usefulness of actors. For a feature that's all about protecting access to mutable state, disallowing the sharing of said state is extremely limiting. Given Swift has no other way to express this sort of functionality, it will be sorely missed.
An aside about "real-world adoption".
In the history of Swift, have deferments for "real-world adoption" ever born fruit? I can't think of any, despite many that should. This sort of decision rings hollow to me.
-
Removal of subtyping, and the strict
asyncnature of the APIs, will make learning, adopting, and using actors more difficult than it needs to be. There's no attempt to allow existing attempts at thread-safety to migrate over to this model. Instead, code must be dumped and largely rewritten if it wants to take advantage of the compiler assured safety. And code which used previous solutions using subtyping can't transition at all. -
async-only APIs aren't enough. Without a way prepend work on the internal executor (or even block while doing so), there's no way to accomplish necessary bits of functionality like cancellation or any mutation that might be depended on by already enqueued work. Instead it seems like any realistic type will need to provide its own blocking for such state updates, greatly diminishing the value of adopting actors in the first place. It shouldn't be necessary to immediately break out of the new model just to accomplish things that already exist. Insisting onasync-only APIs doesn't seem necessary to guarantee safety, given users can already accomplish it manually. Instead it just seems like an attempt to stay inline with the actor model, despite the fact that Swift's actors aren't really actors at all. Adopting a 100%asyncAPI should be a willing design choice for performance or modeling actual async work, it shouldn't be something required because that's the only way to get the language guarantees.
Is the problem being addressed significant enough to warrant a change to Swift?
Yes, this is a great problem to solve.
Does this proposal fit well with the feel and direction of Swift?
No. It is not a pragmatic solution to the problem that fits well with existing Swift. Instead, it feels like a feature designed for a new language that doesn't have existing code to work with and doesn't have to work where Swift already lives.
If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
I've manually implemented types which use underlying DispatchQueues to achieve this sort of safety. It's great, but limiting, so additional functionality to provide synchronous mutation had to be added as well.
How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
Read the pitches and the proposals, been implementing actors with the experimental toolchains.
-
What is your evaluation of the proposal?
+1, removing the legacy inheritance model from objc is the right direction and makes it easier to focus on what actors are about. -
Is the problem being addressed significant enough to warrant a change to Swift?
Yes. -
Does this proposal fit well with the feel and direction of Swift?
Yes. -
If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
I worked with Erlang and always enjoyed to simplicity of the language. I like the fact that actors now focus on their purpose. -
How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
About a day.
A primary concern I have that I haven't seen mentioned is how we test actors without inheritance. Actors, like classes, will likely be used to manage state. From experience, the stateful reference types in programs often form a dependency chain connected to some root singleton or singletons, which are then injected into parts of the program. One of the nice things about Swift's classes, is it's very simple to create a mock subclass inside a test function, only overriding what's needed for the test. This extends to integration tests where you can chain together these inline-mocks (in the fashion expected by the application) and have very clear and concise tests written out for entire application sub-systems. I see a future where these same stateful reference type chains exist, but often as actors injected into parts of the program.
I've read previous arguments that POP is the preferred way to structure subtyping, and I have no general argument there. But in application development, in the case I'm considering, there's almost never any more than 1 primary type for 1 job in the stateful dependency chain, in which case, adding a protocol is redundant. Additionally, external libraries (such as http networking) are usually included in that chain, and often use classes as their single interface. It's then simple to test the libraries and their integration given the method mentioned earlier.
Considering this, I would very much prefer a way to subclass/subactor, if only to test actors. I think Chris' suggestion (2) "Support for inheritance but without designated/required initializers, class methods, etc." would work for most cases, as long as (a) functions can be overridden and (b) mutable test variables can be added, to the sub-actor.
Related to re-entrancy, is it possible to have a one-way message send to an actor’s mailbox? I’m thinking of how Erlang-style actors work. How would I invoke a Void-returning method on an actor without waiting for it to complete? The only thing I can think of is detach { actor.voidMethod(arg) }.
You're very right that this is a crucial piece and quite necessary sometimes.
The proposals so far have been avoiding this topic so you're right that there is no good API to achieve this in the existing proposals.
There is work in progress and to be pitched work which will address this over here though: https://github.com/apple/swift/pull/37007 (will get it's own evolution thread, let's not hyper focus on it just yet please
)
While it is aimed to solve the "call async from sync" it also solves the issue of "send without waiting" (and I'd honestly lobby to call this operation send, but let's chat once it gets it's SE review).
With that proposal, the closure is run on the same executor as the enclosing context; so if used from an actor, the enclosing context is the actor's serial executor, this means that:
async (or send) {
await worker.hello(1)
} // -> Void
async (or send) {
await worker.hello(2)
} // -> Void
would do the right thing here.
It is very important to not devolve into using detach for such things, because a detach would lose: priority, executor, and also any task-locals attached to the calling task that you'd most definitely want to keep propagating.
I have yet to figure out if and how to solve "no need to even send back the result" in the distributed actor setting... but we'll figure that out and I'm pretty sure it could fit the above async (or send) API.
@ktoso Can you help to raise this concern in the second review process of the core team? I think this naming issue is not trivial. Thanks.
I don't really have any other powers than posting in threads or reaching out directly to the authors - same as you can here. The core team, and review manager, go through those threads and put it before the core team.
For what it's worth, personally, I myself do quite like Actor and find AnyActor unnecessarily ugly. The consistency argument falls a bit flat to me... There's also going to be DistributedActor and I honestly don't want to be talking in APIs about : AnyDistributedActor - meh.
Thanks for your feedback on this issue. I just hope that more arguments and explanations could be put on the table for why this proposal prefers Actor over AnyActor.
To provide counter-example for your personal argument, in Swift we already have AnyRandomAccessCollection, AnyBidirectionalCollection and other type-erased definitions. So from my side AnyDistributedActor aligns with the existing naming patterns in Swift. IMO beautifulness or ugliness can change over time, but consistency is more important in the long run ![]()
You can always @ the core team author(s) here (tho I personally don't really like to do it when reviewing).
I’m pretty sure it’s on the table, under this pile of other stuff… but the reason is quite clear: Actor is not a type-erasing wrapper or magic type, it’s “just” a protocol (that can’t be conformed to explicitly). As far as I’m aware, there are no protocols named Any... in the standard library.
What do you mean by "that can't be conformed to explicitly"? I suppose we can define a new protocol that conforms to Actor, and any actor type automatically conforms to the Actor protocol. This is exactly the same behavior as AnyObject on the class types. So either AnyObject should be renamed to Object, or Actor should be renamed to AnyActor. Breaking the naming pattern doesn't make sense to me.
Take the definition of Actor in the proposal:
protocol Actor : AnyObject, Sendable {}
It would make the naming pattern more consistent if it goes like this:
protocol AnyActor : AnyObject, Sendable {}
And if I understand the proposal correctly, the Actor protocol should goes into the same place as Any and AnyObject. So even there are some minor (IMO) difference between the usage of these protocols, we should keep a consistent naming pattern here.
cc @Douglas_Gregor (sorry for the bothering, but I really think the naming should be further discussed)