What's the main difference between top-level @actorIndependent(unsafe)
and @someGlobalActor
?
Does unsafe
mean there's NO lock/queue sync mechanism but globalActor
has it built-in which lead to executor hop
?
What's the main difference between top-level @actorIndependent(unsafe)
and @someGlobalActor
?
Does unsafe
mean there's NO lock/queue sync mechanism but globalActor
has it built-in which lead to executor hop
?
That's right. A global actor is still an actor, it's just globally scoped â there's always exactly one that actor, and you don't have to have a specific reference to it to use it, which gives us a lot more language flexibility to tie things to it. It turns out that that's a pretty common case in concurrent systems and it's worth giving it special attention. So @MyGlobalActor
means that a function is supposed to execute on that specific global actor, whereas actor-independent means it doesn't care what actor it executes on.
Questions and comments from me and my colleagues at Google (most notably @gribozavr, @saeta, and @pschuh):
self
, but then why not have instanceprivate
and mandate that synchronous actor methods use that level of access control? Or, we could not introduce instanceprivate
but mandate that synchronous actor methods are at least labeled private
while also being inaccessible from other instances. If these things are truly separate, presumably you can (meaninglessly) label those methods public or internal without causing an error, which seems wrong.inout parameters
âActor-isolated stored properties can be passed into synchronous functions via inout
parameters, but it is ill-formed to pass them to asynchronous functions via inout
parameters.ââitâs not obvious to me that this restriction is needed or desirable, since presumably
await balance = computesAsynchronously(balance)
would be legal. IMO they are both semantically dubious because of reentrancy of actors, but if we're going to allow the above I don't see why we don't just transform the inout
version into that code and allow it too.
Actor
a base class.actor
classes can conform to the Actor
protocol, and are not subject to the restrictions above.ââAside from the fact that the benefits of this wrinkle arenât very well-justified by the proposal, just from a âreading, thinking, and speaking about the codeâ perspective having an actor
keyword separate from the Actor
protocol seems like a terribly confusing thing to do to programmers. We wonât even be able to talk sensibly about whether something âis-an actorâ without a whole bunch of qualification.static let shared = SomeActorInstance()
ââdoesnât this thing have to be an instance of UIActor
? The way itâs capitalized here, the example seems to be implying it is of type SomeActorInstance
.shared
property that provides the singleton actor instanceââRegardless of what conventions may have been established by Cocoa, the name âshared
â seems like a terrible way to distinguish a singleton instance, since all class instances are âsharedâ in the same sense. May I suggest âsingleton
?â@actorIndependent
? Later in the overrides section thereâs an implication that it would be allowed. Maybe we just need to tweak the meaning of âpropagatedâ a bit to accommodate @actorIndependent
.I didn't mean to imply otherwise!
I think that is happening in this thread for sure, but I think there is also a difference of viewpoint in the actual model that is important to explore -- independent of the terminology.
The world view I'm advocating for here is that actor declarations are islands of single threaded-ness with a queue that protects them, and which are always communicated with asynchronously. Actors are an important design pattern and a good "safe default" for most (but not all) concurrent programming abstractions. I think we generally agree about this.
I think the divide here is that I see actors as living among other types of concurrency (pthreads!) and other forms of synchronization (e.g. the concurrent hashtable, things using RCU internally, whatever), both for compatibility reasons but also because actors are not always the right answer. If I understand you above, I think you're advocating that we make the "other synchronization" possibilities part of the actor model. In contrast, I'm arguing that these should be not part of the actor model, but that the actor model should work with them cleanly.
This is a pretty big difference conceptually, and this is why I'm arguing (over on the ActorSendable
thread) that /classes/ can also participate and work with the actor model, and use their own bespoke internal synchronization approaches. If I understand your model correctly, you are advocating for "classes are never passed between concurrent domains", but people should use actors for that, and if the queue is inconvenient, then they can do unsafe things and @actorIndependent
to stop using it.
I don't see why this is a better model - to me, it is muddling the behavior and responsibility of "real actors" by pulling things that are out of model into their design. This approach is also more complicated for the "migration and interoperability of pre-actors" code, which uses a wide range of concurrency methods and synchronization constructs. While some of these can and should move to first class actors (progress!) not all of them can, because actors are simply not the right answer to everything.
I don't see how forcing non-actor things into an actor class makes the model easier to understand, easier to deploy, or easier to work with. The design you are proposing is also more complicated (needing the attributes etc).
Despite the confusion about terminology, I think this really is a huge conceptual difference in the models we are imagining here.
-Chris
@Douglas_Gregor mentioned this when talking about what's disallowed for inout
parameter.
It feels somehow very familiar (it totally makes sense btw). Then I realized that you can mutate a variable inside a sync escaping closure iff you can pass the same variable through an inout
parameter of an async
closure.
There may be some overarching story about variable access here. Though I'll need to think about it a bit more.
Yeah, I'm not sure how the inout
part of this proposal works. I responded over on the async/await thread, but I think the cleanest answer is for cross-actor calls to forbid inout
parameters. I don't feel like I have a strong grasp of the proposed answer though, so I could be missing something big.
-Chris
I've been reading (back and forth, rather aptly) the concurrent concurrency pitches over the weekend and I have found the section on Global actors a little confusing.
Are the following statements correct?:
The @globalActor annotation to a type declaration creates a new form of '@' annotation that matches that type name.
There should only ever be one global actor per process, so the statement above just allows the user to customise the name of the globalActor annotation.
Non-actors should not be able to read mutable properties of Actor instances (unless one creates convince async accessor functions on that actor class).
Or does 'access' as per the pitch mean write-access?
There can be multiple global actors. Different attributes specify different global actors. Those actors act concurrently with each other. What makes them global is that their state is spread globally through the program rather than being tied to a dingle actor class.
If I understand correctly, nothing prevents an actor method to be called while another one is suspended, right ?
Eg: given the following actor, open()
could start running while a former call to open()
or close()
is suspended ?
actor Order {
func getCurrentStatus() async {
// ...
}
func open() async {
await something()
// ...
}
func close() async {
await something()
// ...
}
}
If I understand correctly, nothing prevents an actor method to be called while another one is suspended, right ?
Yeah, what you're describing is re-entrancy and it indeed is quite tricky. Currently the proposal only has fully re-entrant actors, which indeed can make protecting invariants difficult -- or phrased differently re-entrant actors don't protect from "high-level" races.
I have prepared a writeup here intended to be merged into the actors proposal for "revision 2" and be discussed in depth: [Actors] Reentrancy discussion and proposal by ktoso ¡ Pull Request #38 ¡ DougGregor/swift-evolution ¡ GitHub
This is really interesting.
I'm thinking actors should be non-reentrant by default
And that the strategies for reentrant actor might be quite diverse, and evolve in the future. So it might be relevant to allow customisation for this, rather than bake it in the language.
I see 2 path to allow customisation:
Glad to see non-reentrant functions being considered.
From the design you linked to, we would get both reentrant await
s and non-reeentrant ones, and they are spelled the same (depending on the function signature). Iâd prefer if both kinds of await
had a different spelling so itâs clearer what can potentially happen on the side during the await
.
Otherwise copy pasting code with an await could change the meaning drastically.
I like the idea in the updated proposal that actors would be non-reentrant by default and can be made reentrant on a case-by-case basis.
Regarding spelling, if await
is the keyword that defines suspension points and "inserts" reentrancyâwould it make sense to allow await
to be left out, which would create the intuition of a "blocking" call on the actor queue that would prevent interleaved execution? That would also allow more fine-grained control than @reentrant
at the function level.