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 functionf (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, .Threadsjulia> import Actors: spawn, newLinkjulia> f(s, t; w=1, x=1) = s + t + w + x # a functionf (generic function with 1 method)julia> bhv = Bhv(f, 2, w=2, x=2); # create a behavior of f and acquaintancesjulia> bhv(2) # execute it with a communication parameter8
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 endjulia> (a::A)(t) = a.s + a.w + a.x + t # make it a functor, executable with a communication parameter tjulia> bhv = A(2, 2, 2) # create an instanceMain.A(2, 2, 2)julia> bhv(2) # execute it with a parameter8
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
Linkyou cansendit communication arguments and cause the actor to execute its behavior with them.Actors' API functions likecall,execare just wrappers aroundsendandreceiveusing a communication protocol. - If an actor receives wrong/unspecified communication arguments, it will fail with a
MethodError. - With
become!andbecomewe 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 functionLink{Channel{Any}}(Channel{Any}(32), 1, :default)julia> send(myactor) # send it an empty tuple()julia> receive(me) # receive the result2julia> become!(myactor, threadid)Actors.Become(Base.Threads.threadid)julia> call(myactor) # call it without arguments2julia> become!(myactor, (lk, x, y) -> send(lk, x^y)) # an anonymous function with communication argumentsActors.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 result2409344748064316129
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
stdiovariable. - In the Dict-server example a
Dictvariable 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
:guardactor, which will manage access to them. - In more complicated cases of resource sharing you can use a
:genserveractor.
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