Processes
Often in discrete event systems (DES) we find typical sequences of events $\,S_i = \{e_i, e_j, e_k, ..., e_m\}\,$ repeated over and over and carried out asynchronously by similar agents. For example servers in a data center may
- take a job from an input queue,
- process it for a service time,
- release the job and put it into an output queue.
Those typical event sequences are called processes [1]. If processes interact, their event sequences overlap and generate the event sequence $\,S=\{e_1,e_2,e_3, ..., e_n\}\,$ of the greater system.
Processes in DiscreteEvents
- use functions to describe a characteristic sequence $S_i$ and
- run as asynchronous Julia tasks.
They keep their own data, states and code in their function body.
Syntax
Processes execute Julia code, wait or delay on the clock or wait for available resources. For operations regarding time, IO and resources they use:
delay!
: suspend and get reactivated (by the clock) at/after a given time,wait!
: suspend and get reactivated after a given condition becomes true,now!
: transfer IO-operations to the clock, orprint
via the clock,take!
: take an item from a channel or block until it becomes available,put!
: put something into a channel or block if it is full until it becomes available.
The first three commands: delay!
, wait!
and now!
schedule events to the clock, take!
and put!
on channels are handled directly by the Julia scheduler. All blocking commands should only be used in processes (asynchronous tasks) and never within the main loop (e.g. event- or activity-based implementations) from the Julia REPL [2].
In the following code snippet the two functions define processes:
# describe the server process
function server(clk::Clock, id::Int, input::Channel, output::Channel, X::Distribution)
job = take!(input)
print(clk, @sprintf("%5.3f: server %d serving customer %d\n", tau(clk), id, job))
delay!(clk, X)
print(clk, @sprintf("%5.3f: server %d finished serving %d\n", tau(clk), id, job))
put!(output, job)
end
# describe the arrivals process
function arrivals(clk::Clock, queue::Channel, N::Int, X::Distribution)
for i = 1:N # initialize customers
delay!(clk, X)
put!(queue, i)
print(clk, @sprintf("%5.3f: customer %d arrived\n", tau(clk), i))
end
end
- to run as processes, functions must have a
Clock
variable as their first argument, - processes (or asynchronous tasks in general) must
print
via the clock in order to avoid clock concurrency.
We can start multiple instances of those processes representing e.g. multiple servers or different arrival processes.
Startup
To register functions as processes to the clock and start them we use:
Prc
: prepare a function to run as a process,process!
: register aPrc
to a clock, start it in a loop as an asynchronous task and return its id.
The following code example starts our processes (we assume the variables to be defined before):
for i in 1:c
process!(clock, Prc(i, server, i, input, output, M₂))
end
process!(clock, Prc(0, arrivals, input, N, M₁), 1)
The server
processes run their function body in an infinite loop (default) while the arrivals
process runs it only once and then terminates. The server
processes wait for jobs in their input channels and the arrivals
process waits for the clock to tick. If we start the clock, the processes begin to interact: customers are produced by the arrivals process, the servers then serve them …
Note that the clock variable given to the process function is computed dynamically during process startup. If you start a process on a parallel thread with cid
or spawn
, the process runs on a parallel clock. Therefore in Prc
you don't include the clock variable an an argument.
Limitations
Interacting sequential processes depend on "a rendezvous between the processes involved in sending and receiving the message, i.e. the sender cannot transmit a message until the receiver is ready to accept it". [3] In our case the "message" is a reactivation of a blocked process by the clock or by the Julia scheduler.
If typical event sequences $S_i$ (e.g. waiting for a resource or time delays) are interrupted by stochastic events (e.g. server breakdowns, customers reneging …), the processes are not ready and must use exception handling to tackle them. This means that we have to treat natural occurrences in a stochastic DES as errors in our program.
This a severe limitation of "processes" as used in simulations. If we have to handle different interrupting events requiring different responses, things are getting complicated. This is a barrier to represent more complex systems with sequential processes.
There is an example showing how to handle interrupts to processes.
- 1see: Banks, Carson, Nelson, Nicol: Discrete-Event System Simulation, 4th ed, 2005, p. 74-77
- 2If you want to use
take!
orput!
on channels inside the main program, make sure that they are available (withisready(ch)
orlength(ch.data) < ch.sz_max
) before calling them in order to avoid blocking. - 3see: Communicating Sequential Processes (CSP) on Wikipedia.