Each Actor is a Smalltalk Process. There are two important subclasses
ActorProcess, which implements a subset of the Erlang
process model; and
Actor, which goes a step further, adding a
convention for using
Message objects for RPC and using ordinary
objects as actor behaviors.
Most programs will use
No process isolation is implemented. This is a big difference from languages like Erlang. All Smalltalk objects coexist in a single mutable shared heap. This means that it is very easy to accidentally pass mutable objects between actors. See below.
Actor: Concurrent Smalltalk objects
Actor’s behavior is specified by a distinct
behavior object, usually (but not always!) a
Actor is a subclass of both
inherits the public interfaces of both. See
the section on
below for details.
ActorBehavior subclass: #SimpleTestActor instanceVariableNames: '' classVariableNames: '' poolDictionaries: '' category: 'Actors-Tests'. a := SimpleTestActor spawn. "an ActorProxy for an Actor (92060) on a SimpleTestActor" a := Actor bootProxy: [ SimpleTestActor new ]. "Equivalent to the previous line"
Actors are created with
Actor class >> #boot: and friends, or with
the convenience method
ActorBehavior class >> #spawn, if the desired
behavior object is an
boot: is used, the result is a (running)
Actor class >> #bootProxy:) is used, the result is an
ActorProxy instance, which automatically performs many of the parts
of the RPC protocol that
Actor instances expect. See the section on
See below for a complete list of available constructors.
ActorProxy for an
Actor, requests can be sent with
ordinary message syntax. They are handled by methods on the behavior
object. In this example,
a’s behavior object is a
which has the following method on it:
SimpleTestActor >> addOneTo: aNumber ^ aNumber + 1
This allows us to send requests like this:
a addOneTo: 1. "a Promise" (a addOneTo: 1) wait. "2"
a async addOneTo: 1. "nil" a sync addOneTo: 1. "a Promise" "(like the default)" a blocking addOneTo: 1. "2"
See the section on interaction patterns for more information on why and when you might want to use each of these variations.
Under the covers, a proxy builds an
instance and sends it as a message to the proxy’s actor. If programs
Actor >> #sendMessage:, they must do the same.
Of course, any uncaught exception from the behavior object causes immediate, permanent termination of the actor, and rejection of all outstanding and future requests.
Given the following method:
SimpleTestActor >> divideOneBy: aNumber ^ 1 / aNumber
The following request will cause
a to crash:
a divideOneBy: 0. "a Promise"
The resulting promise will be rejected, with an
ZeroDivide exception as an error value. All subsequent
requests to the now-dead actor will also be rejected with a similar
ActorTerminated object. See the section on
error handling for more details.
If the promise is waited for, a
BrokenPromise exception will be
(a divideOneBy: 0) wait. "Signals BrokenPromise" a blocking divideOneBy: 0. "Signals BrokenPromise"
Implementing a behavior
Behaviors must take care to distinguish between three important objects:
selfis the behavior object, whose methods are invoked by its corresponding
Actor currentis the currently-executing
Actor meis an
ActorProxyfor the currently-executing
A behavior may freely invoke methods on
self, but must take care
when performing RPC using
ActorProxy >> #blocking or
#wait, lest it deadlock: while an actor is blocked, waiting for a
reply to an RPC request, it does not process incoming requests.
If a method on a behavior object returns
self, the request that led
to the method call is answered with
Actor me in place of the
behavior object. No other translation of request messages, reply
messages, or exception values takes place as they travel back and
forth between actors. See the section on
weaknesses for more information.
Terminating an actor
An actor’s behavior object may perform a “normal” exit via
Actor current terminate.
This terminates the currently-executing actor with
nil as an
exit reason. Alternatively, any uncaught
exception terminates the actor abnormally with the exception as its
Actor current kill. "Terminates with a generic exception." self error: 'Oh no!'. "Any other exception will work." 1 / 0. "Ordinary exceptions do the same kind of thing."
Whenever an actor terminates for any reason, normally or abnormally, all its outstanding requests are rejected, as are any requests that may be sent to it in future.
An actor may be terminated from the outside, as well: if an actor
ActorProxy for another actor, it can cause the other
actor to terminate abnormally by executing
p actor kill.
kill is a method on
Actor, not a method on
behavior object. See the section on proxies for more
information on the
actor method of
Cleaning up associated resources
If an actor’s behavior object responds to
method is called after the actor has terminated. The argument passed
to the method is the
exitReason of the terminating actor.
The method runs in a fresh, temporary process, not in the actor’s own
process. By the time of the call to
postExitCleanup:, the actor’s
own process is guaranteed to have terminated. See
ActorProcess: Erlang-style processes
ActorProcess implement a “process style” actor, in the
De Koster et al..
Specifically, they implement a subset of the Erlang approach to the
actor model, providing
- a main process routine;
- a “receive” operation;
- a form of selective receive;
- distinct system-level and user-level messages; and
- Erlang-style “links” and “monitors”.
They are more general than
Actor instances, in that they do not
enforce any particular convention for the user-level messages
exchanged by the actor. However, they are awkward to use directly.
It is almost always better to use
Actor with a custom
ActorBehavior object instead of using
The main process routine
ActorProcess (as opposed to an
Actor) is started with
ActorProcess class >> #boot: and friends:
a := ActorProcess boot: [ "... code ..." msg := ActorProcess receiveNext. "... more code ..." ]. a sendMessage: 'Hello!'.
Like any other
ActorProcess can be
ActorProcess terminates, normally or abnormally, its
links and monitors fire, letting interested
peers know that it has died.
If an uncaught exception is signalled by the main process routine, the actor is terminated permanently. See the section on error handling for more details.
ActorProcess instance can be
Actor class >> #current, and an
ActorProxy for the
currently-executing actor can be retrieved via
Actor class >> #me.
Actor current. "an Actor (79217) on a SimpleTestActor" Actor me. "an ActorProxy for an Actor (79217) on a SimpleTestActor"
System-level and user-level messages
Messages exchanged among
ActorProcess instances come in two kinds:
system- and user-level.
System-level messages are a private implementation detail. They manage
things like links and monitors, and a specific type of system-level
message carries user-level messages back and forth. See senders of
performInternally: to discover the types and uses of system-level
messages. System messages are comparable to (and inspired by)
Erlang’s system messages.
User-level messages are the things sent by
sendMessage: and received by
ActorProcess class >> #receiveNext.
They are a public aspect of working with Actors.
Programmers design their Actors in terms of the exchange of user-level messages, and never in terms of system-level messages.
While any object can be sent as a user-level message between Actors,
the convention is that instances of
are the only kind of user-level message exchanged.
“Internal” and “External” protocols
Unlike ordinary Smalltalk objects, which have public methods and
ActorProcess instances have three kinds of
- public, “external” methods, for use by other actors and processes;
- public, “internal” methods, for use only by the actor itself;
- and ordinary private methods, part of the actor system implementation.
External methods include
isActor and so on. Internal methods include
Actor and ActorProcess constructors
There are many different ways to start an actor.
ActorBehavior spawn. ActorBehavior spawnLink. ActorBehavior spawnLinkName: aStringOrNil. ActorBehavior spawnName: aStringOrNil.
These constructors first instantiate their receiver (a subclass of
ActorBehavior), and then pass the result to one of the
variations on class
Actors with a behavior object
Actor bootLinkProxy: aBlock. Actor bootLinkProxy: aBlock name: aStringOrNil. Actor bootProxy: aBlock. Actor bootProxy: aBlock name: aStringOrNil.
These constructors produce
Actors having the result of evaluating
aBlock as their behavior object. The variations with
Link in the
name link the new actor to the
calling process, and if a
name is supplied, it is used when printing
the actor and in the Squeak process browser.
Actor for: anObject. Actor for: anObject link: aBoolean.
These constructors produce
anObject as their behavior
ActorProcess boot: aBlock. ActorProcess boot: aBlock link: aBoolean. ActorProcess boot: aBlock link: aBoolean name: aStringOrNil. ActorProcess boot: aBlock priority: anInteger link: aBoolean name: aStringOrNil.
These constructors produce actors running
aBlock as their main
routine. Generally speaking,
aBlock will use
Actor receiveNext to
explicitly receive and handle incoming user-level messages.
aBoolean is absent or
false, the new actor will not be
linked by default to the calling
process; if it is present and
true, it will be linked to the calling
If a name is supplied, it is used as the
Process name, which is
displayed in the Squeak process browser and anywhere that the
Actor boot: aBlock. Actor boot: aBlock link: aBoolean. Actor boot: aBlock link: aBoolean name: aStringOrNil. Actor boot: aBlock priority: anInteger link: aBoolean name: aStringOrNil.
Actor is a subclass of
ActorProcess, it inherits these
methods. However, it reinterprets the meaning of
aBlock: instead of
aBlock enacting the main routine of the new actor, it is expected to
return a value that will be used as the behavior object of the new
actor, and the standard
Actor mainloop (
Actor >> #dispatchLoop)
will be used as the main routine.
Weaknesses of the design
Smalltalk poses a number of challenges to implementation of the actor model.
As has already been mentioned, chief among them is the total lack of process isolation. This means that, if a mutable object is accessible by two or more running actors, you still end up having to worry about concurrent access to data structures, even though the actor model is supposed to eliminate this as a programming concern.
An example of a distortion induced by this problem can be seen in the
use of the
copy method in
ChatRoom >> #tcpServer:accepted:. When a
new user connects to the chat room, they are sent a list of the
agent initialNames: present copy.
The chat room must take care to send a copy of
present to the new
ChatUserAgent, because of the asynchrony in the system: if it is not
copied before being sent, it may change while in flight!
A second weakness is connected to the way a returned
self is changed
Actor me. It adds a special case for convenience, but a general
solution would ensure greater process isolation by copying (or
otherwise specially treating) mutable values.
A third weakness is that block arguments to methods invoked on an actor’s behavior are not translated, which makes custom control flow awkward. For example, consider the following snippet:
b := Actor bootProxy: [ true ]. (b ifTrue: [ 1 ] ifFalse: ) wait
b is an
Actor with a
Boolean as its behavior. Invoking
not works perfectly, but
ifTrue:ifFalse: doesn’t work, signalling
NonBooleanReceiver. This example demonstrates a leak of some of the
optimization-enabling assumptions the VM makes.
A similar case occurs in the following example:
d := Actor bootProxy: [ Dictionary new ]. a async at: 1 put: 2. a blocking removeKey: 1. "This works fine..." a blocking removeKey: 99. "... but this kills the whole actor."
If, instead, we use
removeKey:ifAbsent:, we can avoid killing the
whole actor, but at the cost of having the
ifAbsent: block execute
in the wrong context. That is, if it runs, it will run in a context
self is the
Actor current and
denote the actor that the client knows as
d. The tutorial on
collections as behavior covers this topic
in more detail.
Perhaps, in future, a special case for wrapping block arguments in an outer block that causes the block to execute in the correct Actor’s context could be added.