Actors
An Actor is a computational entity that, in response to a message it receives, can concurrently:
- send a finite number of messages to other actors;
- create a finite number of new actors;
- designate the behavior to be used for the next message it receives. [1]
YAActL
actors are Julia Task
s running on a computer or in a network, represented by local or remote Link
s, channels over which they receive and send messages [2]. They:
- react to those messages,
- execute a user defined behavior function when they receive certain messages,
- change their behavior upon request,
- update their internal state which influences how they behave.
The following provides an overview of YAActL
actors:
Start
In the simplest case we start an Actor
with a behavior function:
julia> using YAActL, .Threads
julia> act1 = Actor(threadid) # start an actor who gives its threadid
Link{Channel{Message}}(Channel{Message}(sz_max:32,sz_curr:0), 1, :local)
julia> call!(act1) # call it
1
julia> act2 = Actor(parallel(), threadid) # start a parallel actor
Link{Channel{Message}}(Channel{Message}(sz_max:32,sz_curr:0), 1, :local)
julia> call!(act2) # and call it
2
julia> using Distributed
julia> addprocs(1);
julia> @everywhere using YAActL
julia> act3 = Actor(2, println) # start a remote actor on pid 2 with a println behavior
Link{Distributed.RemoteChannel{Channel{Message}}}(Distributed.RemoteChannel{Channel{Message}}(2, 1, 9), 2, :remote)
julia> call!(act3, "Tell me where you are!") # and call it with an argument
From worker 2: Tell me where you are!
Links
When we started our first actor, we got a Link
to it. This represents a local Channel
over which actors can receive and send messages. Our third actor got a link with a RemoteChannel
. Actors are only represented by their links.
Messages
YAActL
actors communicate and act asynchronously on messages. Basically they use only two functions to interact:
They operate on internal messages, all of type Message
, used by the API functions described below.
A user can also implement his own message types and dispatch the actor behavior based on them. For example a user may implement:
struct Pop <: Message
customer::Link
end
struct Push{T} <: Message
content::T
end
Then he can write a function dispatching on them, start an actor with this behavior and send it Pop
or Push
messages.
Behavior
When actors receive
they compose their owned arguments with the received ones and dispatch their behavior function. Then they store the return value in their internal res
variable.
Following further our example:
julia> mystack = Actor(stack_node, StackNode(nothing, Link())); # start an actor with a first argument
mystack
represents an actor with a stack_node
behavior and first argument StackNode(nothing, Link())
. When it eventually receives a message ...
julia> send!(mystack, Push(1)) # push 1 on the stack
..., it executes stack_node(StackNode(nothing, Link()), Push(1))
.
Actor Control
Actors can be controlled with the following functions:
become!
: cause an actor to switch its behavior,cast!
: cause an actor to execute its behavior function,exit!
: cause an actor to terminate,init!
: tell an actor to execute a function at startup,set!
: set an actor's dispatch mode,term!
: tell an actor to execute a function when it terminates,update!
: update an actor's internal state.
Those functions are wrappers to internal messages and to send!
.
Actors can also operate on themselves, or rather they send messages to themselves:
become
: an actor switches its own behavior,self
: an actor gets a link to itself,stop
: an actor stops.
Bidirectional Messages
What if you want to receive a reply from an actor? Then there are two possibilities:
send!
a message to an actor and thenreceive!
theResponse
asynchronously,request!
: send a message to an actor, block and receive the result synchronously.
The following functions do this for specific duties:
call!
an actor to execute its behavior function and to send the result,exec!
: tell an actor to execute a function and to send the result,query!
tell an actor's to send one of its internal state variables.
If you provide those functions with a return link, they will use send!
and you can then receive!
the Response
from the return link. If you don't provide a return link, they will use request!
to block and return the result. Note that you should not use blocking when you need to be strictly responsive.
Using the API
The API functions allow to work with actors without using messages explicitly:
julia> act4 = Actor(parallel(), +, 4) # start an actor adding to 4
Link{Channel{Message}}(Channel{Message}(sz_max:32,sz_curr:0), 1, :local)
julia> exec!(act4, Func(threadid)) # ask it its threadid
2
julia> cast!(act4, 4) # cast it 4
YAActL.Cast((4,))
julia> query!(act4, :res) # query the result
8
julia> become!(act4, *, 4); # switch the behavior to *
julia> call!(act4, 4) # call it with 4
16
julia> exec!(act4, Func(broadcast, cos, pi .* (-2:2))) # tell it to exec any function
5-element Array{Float64,1}:
1.0
-1.0
1.0
-1.0
1.0
julia> exit!(act4) # stop it
YAActL.Stop(0)
julia> act4.state
ERROR: type Link has no field state
Actor Registry
If a parent actor or worker process creates a new actor, the link to it is only locally known. It has to be sent to all other actors that want to communicate with it.
Alternatively an actor link can be registered under a name (a Symbol
). Then any actor in the system can communicate with it using that name.
julia> using YAActL, Distributed
julia> addprocs(1);
julia> @everywhere using YAActL
julia> @everywhere function ident(id, from)
id == from ?
("local actor", id, from) :
("remote actor", id, from)
end
julia> register(:act1, Actor(ident, 1)) # a registered local actor
true
julia> call!(:act1, myid()) # call! it
("local actor", 1, 1)
julia> register(:act2, Actor(2, ident, 2)) # a registered remote actor on pid 2
true
julia> call!(:act2, myid()) # call! it
("remote actor", 2, 1)
julia> fetch(@spawnat 2 call!(:act1, myid())) # call! :act1 on pid 2
("remote actor", 1, 2)
julia> fetch(@spawnat 2 call!(:act2, myid())) # call! :act2 on pid 2
("local actor", 2, 2)
julia> whereis(:act1) # get a link to :act1
Link{Channel{Message}}(Channel{Message}(sz_max:32,sz_curr:0), 1, :local)
julia> whereis(:act2) # get a link to :act2
Link{RemoteChannel{Channel{Message}}}(RemoteChannel{Channel{Message}}(2, 1, 383), 2, :remote)
julia> fetch(@spawnat 2 whereis(:act1)) # get a link to :act1 on pid 2
Link{RemoteChannel{Channel{Message}}}(RemoteChannel{Channel{Message}}(1, 1, 407), 1, :remote)
julia> registered() # get a list of registered actors
2-element Array{Pair{Symbol,Link},1}:
:act2 => Link{RemoteChannel{Channel{Message}}}(RemoteChannel{Channel{Message}}(2, 1, 383), 2, :remote)
:act1 => Link{Channel{Message}}(Channel{Message}(sz_max:32,sz_curr:0), 1, :local)
julia> fetch(@spawnat 2 registered()) # get it on pid 2
2-element Array{Pair{Symbol,Link{RemoteChannel{Channel{Message}}}},1}:
:act2 => Link{RemoteChannel{Channel{Message}}}(RemoteChannel{Channel{Message}}(2, 1, 383), 2, :remote)
:act1 => Link{RemoteChannel{Channel{Message}}}(RemoteChannel{Channel{Message}}(1, 1, 413), 1, :remote)
The registry works transparently across workers. All workers have access to registered actors on other workers via remote links.
Actor Isolation
In order to avoid race conditions actors have to be strongly isolated from each other:
- they do not share state,
- they must not share mutable variables.
An actor stores the behavior function and arguments to it, results of computations and more. Thus it has state and this influences how it behaves.
But it does not share its state variables with its environment (only for diagnostic purposes). The API functions above are a safe way to access actor state via messaging.
Mutable variables in Julia can be sent over local channels without being copied. Accessing those variables from multiple threads can cause race conditions. The programmer has to be careful to avoid those situations either by
- not sharing them between actors,
- copying them when sending them to actors or
- acquiring a lock around any access to data that can be observed from multiple threads. [3]
When sending mutable variables over remote links, they are automatically copied.
Actor Local Dictionary
Since actors are Julia tasks, they have a local dictionary in which you can store values. You can use task_local_storage
to access it in behavior functions. But normally the state variable sta
and argument passing should be enough to handle values in actors.
- 1See: The Actor Model on Wikipedia.
- 2They build on Julia's concurrency primitives
@spawn
,put!
andtake!
onChannel
s. - 3see Data race freedom in the Julia manual.