Introduction
I've been teaching myself Elixir lately, and one of the concepts you come up against soon enough is that of actors, or more specifically the actor model. On the surface actors look a lot like the objects I'm used to in Ruby.
Forget classes, inheritance, or even objects for a moment: those are not the core tenants of OOP. As Sandi Metz and many other renowned OOP advocates have pointed out: object-oriented programming is first and foremost about message passing. The actor model is all about independent actors maintaining state and passing messages to each other. Sounds like objects to me!
And indeed, in a single-threaded program, (and implementation details aside) they are essentially identical. Having said that, most applications and systems that serve humans don't live in in this fantasy land of single-threaded execution. Long-running jobs, blocking IO and high volumes of concurrent connections that if handled naively in a single thread of execution would result in unusably slow applications, memory problems, crashes, and timeouts.
Traditional OOP languages (Java, C++, Ruby,Python, et al.) weren't designed with concurrency as a first-class use case. While they do support the ability to spawn multiple threads, anyone who's done multi-threaded programming with these languages knows how easy it is to introduce race conditions (e.g. data desynchronization or corruption)
The problem
Here's an example situation, albeit contrived, you might encounter in Ruby:
class DeepThought
def initialize(state)
@state = state
end
def meaning_of_life
# This is a long running calculation where
# @state gets mutated frequently to hold
# intermediary calculations
end
end
dt = DeepThought.new(data)
Thread.new { dt.meaning_of_life }
Thread.new { dt.meaning_of_life }
#=> BOOM!
Most conventional OOP languages provide locks or mutexes as the solution to this problem. It's as if the program is saying: "Hey guys, I'm doing stuff with this data, don't touch it until after I'm done".
Personally I'm not satisfied with locks because it relies on the programmer to identify and fix all possible problem areas (of which there can be many). Locks might be okay generally for small programs, but as the complexity of a program increases, relying on human brains is bound to be error prone.
The solution
Enter the actor model. According to wikipedia:
The actor model in computer science is a mathematical model of concurrent computation that treats "actors" as the universal primitives of concurrent computation. In response to a message that it receives, an actor can: make local decisions, create more actors, send more messages, and determine how to respond to the next message received. Actors may modify private state, but can only affect each other through messages (avoiding the need for any locks).
Whereas you instantiate objects in the current thread of execution, you can think of creating actors as spawning a new thread/process for each actor created. Not only do actors persist indefinitely in their own thread, they also have their own dedicated memory space and share no memory with other actors.
If you'll permit the metaphor: actors are their own islands, and can only communicate via message-in-a bottle across the vast ocean that separates them. Effectively, they may as well be running on different machines (this is important, as we'll see later).
If you remember nothing else, remember this: threads share state, actors share nothing.
Figure 1. Threads share memory and state
Figure 2. Actors have their own segregated memory space and only communicate via messages
Implications
At this point, maybe you get it. Or maybe you're wondering: why do I care? What is the practical application for actors?
Conceptual. I really like the share nothing philosophy of actors. I think this represents the intent of OOP in a purer way. The way classical OOP is implemented in many languages simply doesn't take into account parallelism and introducing multi-theading "breaks" that paradigm. If nothing else I believe actors are simply a better implementation of objects.
Concurrency. As mentioned earlier: since actors have no shared state, there can be no race conditions. As a result, thinking about and implementing concurrent systems becomes simpler and more carefree.
Distribution. The second implication of share nothing is that technically actors don't have to live on the same machine. In fact, certain implementations of the actor model (like the Erlang VM) let you spawn actors transparently on different nodes. That is the beauty of the actor model: it redefines what concurrency is. Traditionally concurrency is thought of as using multiple cores on one machine at the same time. In the world of actors, the concept of concurrency not only includes scaling across CPU cores, but scaling across a computer network.
Conclusion
Hopefully this post helped you clarify the difference between actors and objects. If you haven't already, I would highly recommend you check out Elixir, a next-generation language built on top of the old but powerful BEAM (Erlang VM). And if the functional programming aspect doesn't appeal to you at first, remember this: Elixir is really OOP done right.
I'm a full-stack, functional software developer with a penchant for design who loves elegant solutions and getting shit done. Need help with a project? Get in touch!