Actor Behavior
An actor embodies the three essential elements of computation: 1) processing, 2) storage and 3) communication[1]. Its behavior therefore can be described as $f(a)[c]$, representing
- $f$: a function, processing,
- $a$: acquaintances, storage, data that it has,
- $c$: communication, a message.
It processes an incoming message $c$ with its behavior function $f$ based on its acquaintances $a$.
When an Actor receives a message, it can concurrently:
- send messages to ... addresses of Actors that it has;
- create new Actors;
- designate how to handle the next message it receives. [2]
Gul Agha described the behavior as a ...
... function of the incoming communication.
Two lists of identifiers are used in a behavior definition. Values for the first list of parameters must be specified when the actor is created. This list is called the acquaintance list. The second list of parameters, called the communication list, gets its bindings from an incoming communication. [3]
A behavior then maps the incoming communication to a three tuple of messages sent, new actors created and the replacement behavior:
\[\begin{array}{lrl} f_i(a_i)[c_i] & \rightarrow &\{\{\mu_u,\mu_v, ...\},\;\{\alpha_x,\alpha_y,...\},\;f_{i+1}(a_{i+1})\} \quad\\ \textrm{with} & f: & \textrm{behavior function} \\ & a: & \textrm{acquaintances,} \\ & c: & \textrm{communication,} \\ & \mu: & \textrm{messages sent,} \\ & \alpha: & \textrm{actors created.} \\ \end{array}\]
Behavior Representation in Julia
Actors
expresses actor behavior in a functional style. Actors are basically function servers. Their behavior is a partial application of a callable object $f(a...,c...)$ to acquaintances $a...$, that is, a closure over $f(a...)$. If the actor receives a communication $c...$, the closure invokes $f(a...,c...)$. The ...
-operator allows us to use multiple acquaintance and communication arguments (i.e. lists).
julia> f(a, c) = a + c # define a function
f (generic function with 1 method)
julia> partial(f, a...; kw...) = (c...) -> f(a..., c...; kw...)
partial (generic function with 1 method)
julia> bhv = partial(f, 1) # partially apply f to 1, return a closure
#2 (generic function with 1 method)
julia> bhv(2) # execute f(1,2)
3
Similar to the partial
above, Bhv
is a convenience function to create a partial application ϕ(a...; kw...)
with optional keyword arguments, which can be executed with communication arguments c...
:
julia> using Actors, .Threads
julia> import Actors: spawn, newLink
julia> f(s, t; w=1, x=1) = s + t + w + x # a function
f (generic function with 1 method)
julia> bhv = Bhv(f, 2, w=2, x=2); # create a behavior of f and acquaintances
julia> bhv(2) # execute it with a communication parameter
8
Object-oriented Style
Alternatively we define an object with some data (acquaintances) and make it callable with communication parameters:
julia> struct A # define an object s; w; x # with acquaintances end
julia> (a::A)(t) = a.s + a.w + a.x + t # make it a functor, executable with a communication parameter t
julia> bhv = A(2, 2, 2) # create an instance
Main.A(2, 2, 2)
julia> bhv(2) # execute it with a parameter
8
Actor Operation
When we create an actor with a behavior by using spawn
, it is ready to receive communication arguments and to process them:
- You can create an actor with anything callable as behavior regardless whether it contains acquaintances or not.
- Over its
Link
you cansend
it communication arguments and cause the actor to execute its behavior with them.Actors
' API functions likecall
,exec
are just wrappers aroundsend
andreceive
using a communication protocol. - If an actor receives wrong/unspecified communication arguments, it will fail with a
MethodError
. - With
become!
andbecome
we can change an actor's behavior.
julia> me = newLink()
Link{Channel{Any}}(Channel{Any}(32), 1, :local)
julia> myactor = spawn(()->send(me, threadid()),thrd=2) # create an actor with a parameterless anonymous behavior function
Link{Channel{Any}}(Channel{Any}(32), 1, :default)
julia> send(myactor) # send it an empty tuple
()
julia> receive(me) # receive the result
2
julia> become!(myactor, threadid)
Actors.Become(Base.Threads.threadid)
julia> call(myactor) # call it without arguments
2
julia> become!(myactor, (lk, x, y) -> send(lk, x^y)) # an anonymous function with communication arguments
Actors.Become(Main.var"#4#5"())
julia> send(myactor, me, 123, 456) # send it arguments
(Link{Channel{Any}}(Channel{Any}(32), 1, :local), 123, 456)
julia> receive(me) # receive the result
2409344748064316129
In setting actor behavior you are free to mix the functional and object oriented approaches. For example you can give functors further acquaintance parameters (as for the players in the table-tennis example). Of course you can give objects containing acquaintances as parameters to a function and create a partial application with Bhv
on them and much more.
Actors Don't Share State
Actors must not share state in order to avoid race conditions. Acquaintance and communication parameters are actor state. Actors
does not disallow for an actor to access and to modify mutable variables. It is therefore left to the programmer to exclude race conditions by not sharing them with other actors or tasks and accessing them concurrently. In most cases you can control which variables get passed to an actor and avoid to share them.
Note that when working with distributed actors, variables get copied automatically when sent over a Link
(a RemoteChannel
).
Share Actors Instead Of Memory
But in many cases you want actors or tasks to concurrently use the same variables. You can then thread-safely model those as actors and share their links between actors and tasks alike. Each call to a link is a communication to an actor (instead of a concurrent access to a variable). See How to (not) share variables for a receipt.
In the Actors documentation there are many examples on how actors represent variables and get shared between actors and tasks:
- In the table-tennis example player actors working on different threads share a print server actor controlling access to the
stdio
variable. - In the Dict-server example a
Dict
variable gets served by an actor to tasks on parallel threads or workers. - In the Dining Philosophers problem the shared chopsticks are expressed as actors. This avoids races and starvation between the philosopher actors.
- In the Producer-Consumer problem producers and consumers share a buffer modeled as an actor.
- You can wrap mutable variables into a
:guard
actor, which will manage access to them. - In more complicated cases of resource sharing you can use a
:genserver
actor.
To model concurrently shared objects or data as actors is a common and successful pattern in actor programming. It makes it easier to write clear, correct concurrent programs. Unlike common tasks or also shared variables, actors are particularly suitable for this modeling because
- they are persistent objects like the variables or objects they represent and
- they can express a behavior of those objects.
- 1Hewitt, Meijer and Szyperski: The Actor Model (everything you wanted to know, but were afraid to ask), Microsoft Channel 9. April 9, 2012.
- 2Carl Hewitt. Actor Model of Computation: Scalable Robust Information Systems.- arXiv:1008.1459.
- 3Gul Agha 1986. Actors. a model of concurrent computation in distributed systems, MIT.- p. 30