visit
Readers are assumed to have at least a passing familiarity with an object-oriented language and an actor-oriented language (e.g. Erlang,
Pony). Note that objects and actors are orthogonal abstractions and
therefore a language may implement both – Pony is an example.
Language abstractions orthogonal to both objects and actors will not
be considered here, e.g. static vs. dynamic typing. Because Pony
mixes objects and actors, this paper will use Erlang as the prototype
for actors in order to draw a sharper contrast between objects and
actors. Further exposition of Pony will require its own paper.
Fundamentally, software is logic interacting with state. Logic is encapsulated in functions (aka methods) and state is visible to one or more
functions. When state is visible to multiple simultaneously active
functions, and one or more of those functions can write that state
(aka shared mutable state), accesses must be coordinated in order to
guarantee consistent behavior.
Both objects and actors are encapsulation abstractions which define a
boundary around logic and state. Figure 1 depicts the structure of a
typical object and a typical actor. An object encapsulates one or
more public methods, zero or more private methods, and internal
state. Each method encapsulates logic and local state. All methods
may call each other and the public methods of other objects to which
they have a reference. The internal state is visible to all methods,
e.g. instance variables. An actor encapsulates one or more functions
and a message queue. Each function encapsulates logic and local state
and all functions may call each other. All functions may receive
messages from the queue and send messages to other actors.
For a given problem domain, the logic and state in an object
implementation and an actor implementation will be substantially the
same – modulo the abstractions made available by the language. The
most salient differences between objects and actors are the entry
points across their encapsulation boundary and the sharing of state
within this boundary.
As depicted in Figure 1, every public method of an object is an entry
point; actors have only a single entry point in the form of a message
queue. Within an object there is mutable state visible to multiple
functions – thus each individual object is potentially an instance
of the shared mutable state problem in microcosm. Within an actor
there is no mutable state visible to multiple functions – the
message queue is visible to multiple functions but it is read only.
Given that public methods of an object can be called at any time in any
order by any external method with a reference to that object, these public methods can be simultaneously active even within a single thread. Therefore the object’s internal state can change while a method is executing even if that method did not change said state. A simple example of this scenario is depicted in Figure 2. In general, the shared mutable
state problem may occur when two or more instances of methods within
the same object are simultaneously live, i.e. have frames on the
stack. Note that these could be instances of the same method. In
order to guarantee this behavior does not occur, access coordination
logic will need to be implemented around the internal state of the
object even for single threaded implementations, e.g. locking. This
definition encompasses recursion but in that situation state
modifications are contained within the same function and are
immediately apparent to the developer.
Messages can be sent to an actor at any time in any order [1] by any actor
with a reference to that actor. Messages are processed serially by
any function within the actor; logic within the function determines
the point at which messages are processed and which specific message
in the queue to process. Because messages are processed serially, the
corresponding logic is executed serially. Thus the actor model does
not exhibit the shared mutable state problem inherent in the object
model.
Even if an object were to be designed to have only a single public method
to mimic the single entry point of actors, that public method can
still be called at any time in any order by any external method with
a reference to that object and therefore the shared mutable state
problem is still present.
For objects, methods external to the object determine when, and in what
order, logic within that object is activated. For actors, logic
internal to the actor itself determines when it will be activated (by
extracting a message from the queue). Thus, fully comprehending the
behavior of an object requires developers to look beyond the object
itself – potentially far beyond. Fully comprehending the behavior
of an actor requires inspection of only the functions within that
actor.
Given that logic within the actor decides when to process the next message
and which message to process, it would be simple to implement logic
to guarantee that invariants are maintained before each message is
processed. Implementing a similar guarantee for objects is more
complicated.
To gain experience with the actor model, I suggest starting with a
language for which the actor model is intrinsic. The actor language
with the largest ecosystem and market footprint is Erlang. Erlang
programs are compiled to a virtual machine that schedules actors.
Pony is a promising new actor language that blends actors, objects,
and capabilities. Pony is a compiled language and an actor scheduler
is linked into the binaries.
When transitioning from an object model with threaded concurrency to an
actor model with message passing concurrency, developers will need to
make a mental realignment. Actor creation is almost as fast as a
function call and this changes the optimal approach to scaling.
Imagine a system that processes messages requiring a sequence of
steps. Rather than having permanently running actors for each step
where each actor serially executes a step in the message processing
sequence, a better approach is to spawn a set of transient actors for
each message when it arrives.
The bottom line: When compared to the actor model, the object model is
more brittle, its encapsulation more permeable, and its cognitive
load on developers higher. To summarize, the advantages of actors
relative to objects are:
[1] Erlang and Pony guarantee that messages sent by actor A to actor
B appear in B’s message queue in the order A sent them.