4 ms·
Actors are useful for keeping code clean/understandable/correct when dealing with heavy concurrency AND lots of mutable state. Actors are like objects, but the
by yashap 4y ago
Actors are useful for keeping code clean/understandable/correct when dealing with heavy concurrency AND lots of mutable state.
Actors are like objects, but the only way to interact with them is sending them a message - you can’t directly access their properties, call their methods, etc. Messages are bits of immutable data sent between actors asynchronously, but those messages go into the actor’s mailbox, and then the actor processes them synchronously, one at a time. You can get parallelism by adding more actors of the same type (10 actors of the same type lets you process up to 10 messages concurrently). Because actors are totally synchronous INTERNALLY, and nobody can directly access their internal state, it’s fine for them to have mutable internal state, with no locking/synchronization needed, and much easier to reason about than mutable state normally is in highly concurrent applications.
With that being said, the actor model has a lot of inherent complexity. If you don’t need it, don’t use it. But if you really do need lots of concurrency and lots of mutable state, it’s often a great choice.
They’re also good for distributed systems. Actors only communicate by sending messages to other actors, but it doesn’t really matter if that message is sent in memory to an actor in the same process, or over the network to an actor on a different machine. So you can take a single actor system and split it across many machines pretty easily. Generally the actor framework you’re using will handle delivering messages over the network, ensuring there’s the right number of actors running across all nodes combined, etc.
- dragonwriter 4y ago> Actors are like objects, but the only way to interact with them is sending a message - you can’t access their properties, call their methods, etc. While the implementation in many popular, e.g. C++ and family descended from it, languages muddies the waters, basically methods in OO are a convenient way to define handlers for messages sent to objects, and in pure OO you also can't interact with objects except by sending messages to them. The difference between the basic OO model and the actor model is that OO message sends are synchronous request/response and actor model message sends are asynchronous fire-and-forget.
- yashap 4y agoAgreed, although it’s not always fire-and-forget (i.e. send a message and don’t wait for a response). Actors generally do support request/response style messaging, but it’s always async - it’s really only synchronous request/response communication that’s not allowed.
- dragonwriter 4y ago> Agreed, although it’s not always fire-and-forget (i.e. send a message and don’t wait for a response). Sure, you can do async request/response in the actor model, but the low-level basic mechanism all communication is built on is send a message to an actor mailbox and keep going. Everything else is built in top of that.
- ryukafalz 4y agoNot always. Goblins[0] is an example of an actor system that supports both synchronous and asynchronous communication between actors, with synchronous communication only possible if both actors are in the same “vat” (a kind of event loop). [0] https://docs.racket-lang.org/goblins/ https://docs.racket-lang.org/goblins/
- paroneayea 4y agoHi! Yeah I'm the author of Goblins :) Yes, Goblins' "vat" model descends from E, which supports the "hybrid worldview". http://www.erights.org/ http://www.erights.org/ I have a tendency to call Goblins objects "actors", though some people in the community like to point out that "distributed object" is preferable since synchronous call/return is not supported in actors. But yeah.
- zo1 4y agoSo much this. This "actor model" thing as described by GP sounds like plain old OO and RPC with extra steps and also distributed. This may be a low-effort post on my part, but I'm 100% not impressed because it sounds like marketing fluff and an opportunity to rewrite native/built-in things using frameworks. Also justifying Yet Another Framework or tool hosted on an .IO domain (snarky I know) for promotions and endless blog posts.
- jfoutz 4y agoI've read about actors from time to time for decades. I can't say I fully grok the concept. Kubelet wasn't necessarily designed as an actor. I think it's a concrete thing that people have interacted with that is a pretty good example of an actor. It has it's own control loop. It sits there and actively monitors the state of the node. It constantly reports the state. If something's wrong, it can try to correct it. Most services and objects just sort of sit around and wait to be called. Kubelet is a little different, it's got a main driving thread that's constantly looking for trouble, and acting on what it finds. The line is still pretty fuzzy for me, but maybe this helps someone connect some conceptual dots, to distinguish between a service and an actor.
- spinningslate 4y ago> those messages go into the actor’s mailbox, and then the actor processes them synchronously Not trying to be pedantic, but that's technically not true of the Actor formalism [0]. Quoting wikipedia: > an actor can designate the behavior to be used to process the next message, and then in fact begin processing another message M2 before it has finished processing M1. Available implementations (such as Erlang, Akka, Pony) do not support that pipelining so are single threaded per actor as you state though. As an aside, the behaviour you describe is part of the CSP formalism [1] - a related but different approach to concurrent systems. [0]: https://en.wikipedia.org/wiki/Actor_model#Inherently_concurrent https://en.wikipedia.org/wiki/Actor_model#Inherently_concurr... [1]: https://en.wikipedia.org/wiki/Communicating_sequential_processes https://en.wikipedia.org/wiki/Communicating_sequential_proce...
- throwawaymaths 4y agoJust to be a bit pedantic (but important), Erlang is not an actor system. It's a system designed for fault tolerance and definable failure domains. Concurrency fell out of those requirements and it just happens to vaguely look like an actor system if you squint hard enough. I don't believe theories of actor systems played into the design of Erlang. Perhaps some of the blame belongs on the creators of Erlang for kind of jumping on the "actor" (and later, "OOP") bandwagons as marketing? to try to make Erlang less scary.
- spinningslate 4y agoFair point: Erlang wasn't designed as an Actor system. But you don't have to squint that hard to see the similarities. Again quoting wikipedia [0]: > An actor is a computational entity that, in response to a message it receives, can concurrently: send a finite number of messages to other actors; create a finite number of new actors; designate the behavior to be used for the next message it receives. > There is no assumed sequence to the above actions and they could be carried out in parallel. All above are true of Erlang except intra-actor concurrency. Even the last bullet on designating behaviour: An Erlang process (actor equivalent) can decide to use a different function when receiving the next message. It just can't pipeline handling messages. So Erlang isn't that far removed from the Actor model in its behaviour, even if it wasn't designed as an Actor system in the first place. One might say actors are an (approximate) emergent property of Erlang rather than an intentionally designed one. Slightly off-topic but the only system I've used that was designed & built from the outset as an actor system is Rosette [1]. It was a really interesting project, and does support pipelining, but has been dormant for more than a decade. [0]: https://en.wikipedia.org/wiki/Actor_model#Fundamental_concepts https://en.wikipedia.org/wiki/Actor_model#Fundamental_concep... [1]: https://github.com/leithaus/Rosette https://github.com/leithaus/Rosette -- EDIT: clarified that Rosette is the only system I've used that was explicitly based on the Actor model from the outset.
- solaxun 4y agoOut of curiosity (not much experience with Actor systems), do these systems solve a different problem than having disparate processes communicate by sticking a distributed message queue between them? From my naive point of view it seems like alot of the scaling and fault tolerance concerns nowadays can be solved in any language, by having a queue be the interface between two services. Same question applies to Erlang, which I realize is not exactly an actor system as per the below comment, and has a much more sophisticated error recovery story with supervisors, but the general question holds.
- lostcolony 4y agoSo I think the Erlang distinction is a little unrelated (I mean, it's technically accurate, but not because of the error recovery; that's an implementation detail. The original Actor formulation Carl Hewitt proposed was heavily influenced by Smalltalk semantics; whereas Smalltalk was truly OO, in that everything is an Object that passed messages, one issue it had was that objects did not equate to threads of execution. The Actor model, then, said each object executed independently. But in the same way that Smalltalk did not have primitives; even integers are always Objects, the Actor model stipulates there are no primitives; even integers are Actors). But I'll take a stab at answering what I think you're asking. If your services don't deal with internal mutable state, nor high degrees of concurrency then there isn't much gain to be had with an actor system. That said, that begs the question of what the queue is for; just create more instances, since there's no internal state to share. As soon as you start having internal mutable state and high levels of concurrency, that's where the actor model applies. Queues don't exist for concurrency (you don't need them; just create more executors), they exist for imposing sequence where it is needed (an obvious case; you have a DB connection, you want to only have one query at a time. So every desired query goes into a queue, and the process at the end that owns the DB connection pulls from it). Internal mutable state gets stored inside of an actor; updates and reads get serialized on that actor. At the highest level, I would describe the actor model as taking a 'successful' model for distributed computing, and making it the only model you use, even locally. In, let's say Java, for instance, using standard concurrency approaches, it matters where a process lives. My way of operating/communicating to another thread of execution (unit of concurrency) is very, very different than my way of operating/communicating to another machine. Locally I have threads and locks and need to be very mindful. When communicating to another machine, I send a message and that's it (maybe I expect a response, and timeout if I don't get one, but that's really just the same thing, the other machine sending a message). I don't actually need a queue involved for theoretical correctness unless I need to process messages in sequence (after all, I could have multiple copies of the other process, and send a message to each of them). Now, in the real world I do, simply because if my concurrency gets too large it can't be handled by what the units of concurrency already available (instances, threads, whatever), and scale up takes time, but that's really just a special case of why I need to impose a sequence on messages (handle these first, then handle these, rather than handle all of them concurrently). The actor model makes this the local model of communication (and so makes the impedance mismatch negligible between local and distributed; so much so that some languages it's actually irrelevant whether you're sending a message to a local actor, or a remote one). Scaling concurrency up internally just means spin up a new actor. When you need to serialize, you send messages to the same actor, where it ends up in a queue. So it's not solving a different problem, exactly, if that problem is "how do we write systems that can do multiple things at once", but the specifics, complexity, etc, tend to be pretty different. The problems it's solving are a bit more subjective than simply "can we handle this problem", and more "how well do our tools and mental model lend themselves to the problem we're trying to solve".
- lostcolony 4y ago>> With that being said, the actor model has a lot of inherent complexity. If you don’t need it, don’t use it. But if you really do need lots of concurrency and lots of mutable state, it’s often a great choice. I kind of disagree with this. I mean, it's kind of correct, if you literally have a sequential problem with immutable state you're gaining nothing from it, but you're also losing nothing (you...will have one actor, no messages being passed, so the only cost is the syntax of the language; the actor model isn't adding any complexity because you aren't using it). But, a lot of problems we've historically learned to view as sequential are in fact concurrent. Deeply concurrent. We've created entire concepts specifically to impose sequential processing upon innately concurrent activities. As an example, almost everywhere we use queues we could instead model as unrelated processes able to be run concurrently. You still may have a bit of queueing/scheduling for unbounded processes, but I know from production experience that that is far, far simpler to do and get right using actors than a traditional queue/priority queue and worker(s). And that's where it shines. It encourages you to start thinking about what in fact -should- be done concurrently, by making that easy rather than a chore as it is in most other languages, and that leads to -less- complexity.
- bcrosby95 4y agoI'm curious what you see as so different in an actor vs a queue with workers. An actor's mailbox effectively acts as a queue. Is it the supporting structure surrounding an actor focused library/language? Lots of person-hours have gone into making them to work well. Some home grown queue + pool of threads working off that queue... not so much.
- ackfoobar 4y agoYou're exactly right. Some examples: - Some common patterns. E.g. actor hierachy and supervision, circuit breaker. - Persistence. The actor persists its state (changes) and can wake up from a sleep. - Clustering. The actor can live in another node, and you interact with it using the same API.
- ackfoobar 4y ago
- gentleman11 4y agoSounds like an implementation of object oriented programming as it was originally envisioned
- activitypea 4y agoExcellent description of the actor model, thank you!
- layer8 4y agoI’m curious about the following: Suppose you have an actor that is tasked to perform a long-running operation (enacted by a specific message sent to the actor). Now suppose you want to know how far the actor has already progressed with the operation, or possibly you want to pause or stop/abort the operation – basically a monitoring and controlling interface. Would that be out of scope for the actor model? Polling the progress or gracefully stopping the operation would seem to require some asynchronous responsiveness from the actor. Is that something that is commonly supported by actor frameworks? If so, how does that work? Is that a gray area where the concrete actor implementation has to deal with concurrency after all, e.g. special callback routines that will be invoked concurrently to the main operation?
- JoeHegarty 4y agoMany actor frameworks (including Orbit, though I advise you don't use it for the reasons in my above messages) are strictly single threaded in that only one thread can operate on an actor at any one time, and by default they are also not concurrent in that only one message gets processed at once. However, while the single-threadedness is a hard requirement and behavior of the system, the rule that only one message is processed at a time is not. In Orbit (and Orleans) the ability to process multiple messages at the same time is called reentrancy. it can be specified at the actor or method level usually. So in your example, say your initial message kicks off a write that is going to take 30 seconds. You start the write and assuming it's async, the actor then suspends while it waits for that to complete. While the actor is suspended another message asking for the progress arrives and that message is reentrant, it can process that message and respond with the progress, then the actor suspends again and at some point another reentrant message arrives or the initial write finishes and the first message finally gets a response. If a none reentrant message is received it will queue in the mailbox until after the first one is completed. So, with reentrancy you still get the single threaded guarantee but you effectively get interleaving of messages when the actor is suspended waiting for some other async task to complete. This means that if you have an actor with reentrant messages you have to make that code safe to run during any potential interleaving point in another message (for example an await in async C#). Hopefully that made sense, Orleans has some docs about reentrancy here you might find useful: https://dotnet.github.io/orleans/docs/grains/reentrancy.html https://dotnet.github.io/orleans/docs/grains/reentrancy.html Other actor frameworks (particularly ones that are not based on virtual actors) may have different guarantees and behaviors but from what I've seen there is always some equivalent.