Actors
DES under investigation, models and simulations tend to get bigger and more complex. Likewise we want to use the available computing power of parallel and distributed systems. Actors are a way to model larger and more complex systems and to use the available computing power.
The Actor Model
Actors are a more powerful computational agent than sequential processes ...
... in the context of parallel systems, the degree to which a computation can be distributed over its lifetime is an important consideration. Creation of new actors provides the ability to abstractly increase the distributivity of the computation as it evolves. [1]
DiscreteEvents
uses Actors to represent entities in DES and to distribute those entities over parallel cores of modern computers.
Actor systems and actors have the following basic characteristics:
- Communication via direct asynchronous messaging: ...
- State machines: Actors support finite state machines. When an actor transitions to some expected state, it can modify its behavior in preparation for future messages. By becoming another kind of message handler, the actor implements a finite state machine.
- Share nothing: Actors do not share their mutable state ...
- Lock-free concurrency: ... actors never need to attempt to lock their state before reacting to a message. ...
- Parallelism: ... Parallelism with the Actor model tends to fit well when one higher-level actor can dispatch tasks across several subordinate actors, perhaps even in a complex task processing hierarchy.
- Actors come in systems: ... [2]
Actors integrate well into an event framework because they are message driven. Messages signal that an event has happened. Therefore actors are more reactive to events than sequential processes. Furthermore actor systems allow to represent and compose DES in a new way. They can
- represent hierarchy (e.g. UML state machines, different system levels ...),
- model structural changes in systems (e.g. making more servers available if load gets too high).
An Operational Definition
Given the descriptions above, the DiscreteEvents
framework minimally assumes the following about actors:
- An actor is represented by a Julia
Channel
. Any
messages to the actor are sent to it over this channel. An Actor Framework may specify further the type of messages.- The actor is a
Task
listening and responding to messages it receives over that channel. - The actor is responsive to those messages and carries out its actions immediately and without further blocking.
- Therefore any actions by an actor are discrete and can be completed within one time step.
When those assumptions are not met (e.g. actors take longer to complete their work ...) DiscreteEvents
will not work well with actors.
Actor Support
Given those assumptions and to work within the DiscreteEvents
framework, an actor registers (push!
es) its message channel into the clk.channels
vector of the thread-local Clock
variable clk
. When run!
ning, the clock will not proceed to the next time step before all registered actor channels are empty and therefore the associated actors have completed their actions for that time step.
Actors must not use blocking calls like delay!
or wait!
. Instead they work with event!
s to send messages to themselves after some time or under some conditions. In order to get the sequence right, it is recommended that they work as finite state machines. This is illustrated in the M/M/c with State-machines example.
DiscreteEvents
only has minimal actor support and therefore does not depend on any specific actor library.
Dynamical state machines
For the following exploration into actors we use YAActL
Actor
s. They have behaviors described by functions. They can change their behaviors with become
. Thus they assume a new state and act as state machines. They can send delayed messages to themselves over the clock. The following code snippet is taken from the M/M/c with Actors example:
using YAActL, DiscreteEvents
...
Base.get(clk::Clock, m::Message, after, Δt::Distribution) =
event!(clk, fun(send!, self(), m), after, Δt)
...
function idle(s::Server, ::Arrive) # on arrival message
if isready(s.input) # check the input ...
s.job = take!(s.input) # ... in order not to block
become(busy, s) # change behavior
get(s.clk, Finish(), after, s.d) # send delayed message
...
end
end
busy(s::Server, ::Message) = nothing # a default transition
function busy(s::Server, ::Finish)
become(idle, s) # change behavior
put!(s.output, s.job)
...
end
When the actor starts, it takes his initial idle
behavior. It returns a link (a message channel) for sending messages to it, which we register!
to the clock.channels
vector. With an Arrive()
message, it checks for a job and eventually takes it and becomes busy
...
for i in 1:c # start servers/actors
s = Server(i, clock, input, output, 0, M₂)
register!(clock.channels, Actor(idle, s))
end
event!(clock, fun(arrive, clock, input, jobno, clock.channels), every, M₁, n=N)
We then can command the actors over the clock.channels
vector. For the full example see M/M/c Queue with Actors.