6.4 Examples of Process Modeling
The following example illustrates how to use a hold queue via the HoldQueue
class. A hold queue holds an entity within a queue until it is removed. It is important to note that the entity that goes into the hold queue cannot remove itself. Thus, as you will see in the following code, we schedule an event that causes the entities to be removed at a specific time. The hold queue is created and an event action defined to represent the event. The entity process is simple, when the entity enters the process it immediately enters the hold queue. The process will be suspended. After being removed and resumed, the entity continues through a couple of delays.
6.4.1 Holding Entities in a Queue: HoldQueue
Class
Example 6.3 (Illustrating a HoldQueue) This example illustrates how to create an instance of the HoldQueue
class and how to use it to hold entities until they can be released. An event is scheduled to cause held entities to be removed and to resume their processing.
class HoldQExample(parent: ModelElement) : ProcessModel(parent, null) {
private val myHoldQueue: HoldQueue = HoldQueue(this, "hold")
private val myEventActionOne: EventActionOne = EventActionOne()
private inner class Customer: Entity() {
val holdProcess : KSLProcess = process() {
println("time = $time : before being held customer = ${this@Customer.name}")
hold(myHoldQueue)
println("time = $time : after being held customer = ${this@Customer.name}")
delay(10.0)
println("time = $time after the first delay for customer = ${this@Customer.name}")
delay(20.0)
println("time = $time after the second delay for customer = ${this@Customer.name}")
}
}
override fun initialize() {
val e = Customer()
activate(e.holdProcess)
val c = Customer()
activate(c.holdProcess, 1.0)
schedule(myEventActionOne, 5.0)
}
private inner class EventActionOne : EventAction<Nothing>() {
override fun action(event: KSLEvent<Nothing>) {
println("Removing and resuming held entities at time : $time")
myHoldQueue.removeAllAndResume()
}
}
}
The initialize()
method creates a couple of entities and activates their hold process. In addition, the event representing the release from the hold queue is scheduled for time 5.0. In the event logic, we see that the hold queue instance is used to call its removeAllAndResume()
function. The HoldQueue
class is a subclass of the Queue
class. Thus, statistics are collected when it is used. In addition, it can be iterated and searched. If a reference to a particular entity is available, then the removeAndResume()
function can be used to remove and resume a specific entity. These two methods automatically resume the entity’s process. Other methods inherited from the Queue
class allows for the entities to be removed without resuming their processes. It is then the responsibility of the user to properly resume the suspended processes by directly using the entity instance. There are also methods for terminating the held entity’s processes. The output from this simple simulation is as follows:
time = 0.0 : before being held customer = ID_1
time = 1.0 : before being held customer = ID_2
Removing and resuming held entities at time : 5.0
time = 5.0 : after being held customer = ID_1
time = 5.0 : after being held customer = ID_2
time = 15.0 after the first delay for customer = ID_1
time = 15.0 after the first delay for customer = ID_2
time = 35.0 after the second delay for customer = ID_1
time = 35.0 after the second delay for customer = ID_2
We see that the two customers are held in the queue right after activation. Then, the event at time 5.0 occurs which removes and resumes the held entities. The rest of the output indicates the the entities continue their processes.
6.4.2 Signaling Entities
The next example illustrates the use of the Signal
class, which builds off of the HoldQueue
class, as shown in Figure 6.10.

Figure 6.10: HoldQueue and Signal Classes
The Signal
class uses an instance of the HoldQueue
class to hold entities until they are notified to move via the index of their rank in the queue. If you want the first entity to be signaled, then you call signal(0).
The entity is notified that its suspension is over and it removes itself from the hold queue. Thus, contrary to the HoldQueue
class the user does not have to remove and resume the corresponding entity.
Here is some example code. Notice that the code subclasses from ProcessModel.
All implementations that use the process modeling constructs must subclass from ProcessModel.
Then, the instance of the Signal
is created. An inner class implements and entity that uses the signal. In the process, the entity immediately waits for the signal. After the signal, the entity has a simple delay and then the process ends.
Example 6.4 (Illustrating how to Hold and Signal Entities) This example illustrates how to use the Signal
class to hold entities until a signal is sent. Once the signal is received the entity continues its processing.
fun main(){
val m = Model()
SignalExample(m)
m.numberOfReplications = 1
m.lengthOfReplication = 50.0
m.simulate()
m.print()
}
class SignalExample(parent: ModelElement, name: String? = null) : ProcessModel(parent, name) {
private val signal = Signal(this, "SignalExample")
private inner class SignaledEntity : Entity() {
val waitForSignalProcess: KSLProcess = process {
println("$time > before waiting for the signal: ${this@SignaledEntity.name}")
waitFor(signal)
println("$time > after getting the signal: ${this@SignaledEntity.name}")
delay(5.0)
println("$time > after the second delay for entity: ${this@SignaledEntity.name}")
println("$time > exiting the process of entity: ${this@SignaledEntity.name}")
}
}
override fun initialize() {
for (i in 1..10){
activate(SignaledEntity().waitForSignalProcess)
}
schedule(this::signalEvent, 3.0)
}
private fun signalEvent(event: KSLEvent<Nothing>){
signal.signal(0..4)
}
}
The initialize()
method creates 10 instances of the SignalEntity
subclass of the Entity
class and activates each entity’s waitForSignalProcess
process. It also schedules an event to cause the signal to occur at time 3.0. In the signal event, the reference to the signal, signal,
is used to call the signal()
method of of the Signal
class. The first 5 waiting entities are signaled using a Kotlin range. The output from the process is as follows.
0.0 > before waiting for the signal: ID_1
0.0 > before waiting for the signal: ID_2
0.0 > before waiting for the signal: ID_3
0.0 > before waiting for the signal: ID_4
0.0 > before waiting for the signal: ID_5
0.0 > before waiting for the signal: ID_6
0.0 > before waiting for the signal: ID_7
0.0 > before waiting for the signal: ID_8
0.0 > before waiting for the signal: ID_9
0.0 > before waiting for the signal: ID_10
3.0 > signaling the entities in range 0..4
3.0 > after getting the signal: ID_1
3.0 > after getting the signal: ID_2
3.0 > after getting the signal: ID_3
3.0 > after getting the signal: ID_4
3.0 > after getting the signal: ID_5
8.0 > after the second delay for entity: ID_1
8.0 > exiting the process of entity: ID_1
8.0 > after the second delay for entity: ID_2
8.0 > exiting the process of entity: ID_2
8.0 > after the second delay for entity: ID_3
8.0 > exiting the process of entity: ID_3
8.0 > after the second delay for entity: ID_4
8.0 > exiting the process of entity: ID_4
8.0 > after the second delay for entity: ID_5
8.0 > exiting the process of entity: ID_5
We see that at time 0.0, the 10 entities are created and their process activated so that they wait for the signal. Then, at time 3.0, the signal occurs and each of the signaled entities (in turn) resume their processes. Eventually, they complete their process after the 5.0 time unit delay. Notice that since the simulation run length was 50.0 time units, the simulation continues until that time. However, since there are no more signals, the last 5 entities remain waiting (suspended) at the end of the simulation. As previously mentioned, the ProcessModel
class is responsible for removing these entities that are suspended after the replication has completed. Thus, when the next replication starts, there will not be 5 entities still waiting. The KSL takes care of these common clean up actions automatically.
6.4.3 Understanding Blocking Queues
This next example illustrates the use of a blocking queue. Blocking queues are often used in asynchronous programs to communicated between different threads. In the case of KSL process models, we can use the concept of blocking queues to assist with communication between two processes.
Blocking queues can block on sending or on receiving. The typical use is to block when trying to dequeue an item from the queue when the queue is empty or if you try to enqueue an item and the queue is full. A process trying to dequeue from an empty queue is blocked until some other process inserts an item into the queue. A process trying to enqueue an item in a full queue is blocked until some other process makes space in the queue, either by dequeuing one or more items or clearing the queue completely. The KSL provides a class called BlockingQueue
that facilitates this kind of modeling. In the case of the KSL, depending on the configuration of the queue, both the sender and the receiver may block.

Figure 6.11: The BlockingQueue Class
There are actually three queues used in the implementation of the BlockingQueue
class. One queue called the senderQ
will hold entities that place items into the blocking queue if the queue is full. Another queue, called the receiverQ,
will hold entities that are waiting for items to be placed in the queue to be removed. Lastly, is the blocking queue, itself, which is better conceptualized as a channel between the sender and the receiver. This queue is called the channelQ
and holds items that are placed into it for eventual removal. Statistics can be collected (or not) on any of these queues. The sender and receiver queues are essentially instances of the KSL Queue
class. As such, they have no capacity limitation. The channel queue can have a capacity. If a capacity is not provided, it is Int.MAX_VALUE,
which is essentially infinite.
As noted in Figure 6.11 we see that the items within the queue are requested from the receiver. These requests can have a specific amount. The receiver will need to wait until the specific amount of their request becomes available in the queue (channel). Users can provide a selection rule for the requests to determine which requests will be selected for filling when new items arrive within the channel. The default request selection rule is to select the next request. The following code illustrates the basic creation and use of a blocking queue.
Example 6.5 (Illustrating a Blocking Queue) This example illustrates how to use a blocking queue. A blocking queue can cause an entity to wait until an item is available in the channel or cause an entity to wait until there is space in the channel.
You can create a blocking queue with a capacity (or not) and you can specify whether the statistics are collected (or not) as noted by the commented code of the example. In this example, the queue (channel) has a capacity of 10 items.
In the following code, we implement the process for receiving items from the blocking queue. Notice that there is a loop within the process. This illustrates that all normal Kotlin control structures are available within process routines. The entity (receiver) will loop through this process 15 time and essentially wait for single item to be placed in the blocking queue. If the entity hits the waitForItems()
suspending function call and there is an item in the queue, then it immediately receives the item and continues with its process. If an item is not available in the blocking queue when the entity reaches the waitForItems()
suspending function, then the requesting entity will wait until an item becomes available and will not proceed until it receives the requested amount of items. After receiving its requested items, the entity continues with its process.
private inner class Receiver: Entity() {
val receiving : KSLProcess = process("receiving") {
for (i in 1..15) {
println("$time > before the first delay for entity: ${this@Receiver.name}")
delay(1.0)
println("$time > trying to get item for entity: ${this@Receiver.name}")
waitForItems(blockingQ, 1)
println("$time > after getting item for entity: ${this@Receiver.name}")
delay(5.0)
println("$time > after the second delay in ${this@Receiver.name}")
}
println("$time > exiting the process in ${this@Receiver.name}")
}
}
In this code snippet, the entity (sender), also loops 15 times. Each time within the loop, the entity creates an instance of a QObject
and places it in the blocking queue via the send()
method. Since the blocking queue has capacity 10, and the time between requests by the receive is a little longer than the time between sends, the blocking queue will reach its capacity. If it does reach its capacity, then the sender will have to wait (blocking) at the send()
suspending function until space becomes available in the channel.
private inner class Sender: Entity() {
val sending : KSLProcess = process("sending") {
for (i in 1..15){
delay(5.0)
println("$time > after the first delay for sender ${this@Sender.name}")
val item = QObject()
println("$time > before sending an item from sender ${this@Sender.name}")
send(item, blockingQ)
println("$time > after sending an item from sender ${this@Sender.name}")
}
println("$time > exiting the process for sender ${this@Sender.name}")
}
}
override fun initialize() {
val r = Receiver()
activate(r.receiving)
val s = Sender()
activate(s.sending)
}
}
In the intialize()
method, single instances of the receiver and sender are created and their processes activated. The model is setup to run for 100 time units
fun main(){
val m = Model()
val test = BlockingQExample(m)
m.lengthOfReplication = 100.0
m.numberOfReplications = 1
m.simulate()
m.print()
}
The output of the simulation is illustrative of the coordination that occurs between the receiver and the sender. We see in this output that the the receiving entity blocks at time 1.0. Finally, at time 5.0 the sender sends an item and it is received by the receiving entity. Both processes continue.
0.0 > before the first delay for receiving entity: ID_1
1.0 > trying to get item for receiving entity: ID_1
5.0 > after the first delay for sender ID_2
5.0 > before sending an item from sender ID_2
5.0 > after sending an item from sender ID_2
5.0 > after getting item for receiving entity: ID_1
10.0 > after the first delay for sender ID_2
10.0 > before sending an item from sender ID_2
10.0 > after sending an item from sender ID_2
10.0 > after the second delay for receiving entity: ID_1
10.0 > before the first delay for receiving entity: ID_1
.
.
.
The queueing statistics indicate that the sender never blocked and the receiver had a small amount of blocking.
Half-Width Statistical Summary Report - Confidence Level (95.000)%
Name Count Average Half-Width
-----------------------------------------------------------------------------------------
BlockingQueue_4:SenderQ:NumInQ 1 0.0000 NaN
BlockingQueue_4:SenderQ:TimeInQ 1 0.0000 NaN
BlockingQueue_4:RequestQ:NumInQ 1 0.2100 NaN
BlockingQueue_4:RequestQ:TimeInQ 1 0.3077 NaN
BlockingQueue_4:ChannelQ:NumInQ 1 0.3300 NaN
BlockingQueue_4:ChannelQ:TimeInQ 1 2.2000 NaN
-----------------------------------------------------------------------------------------
As illustrated in this example, a blocking queue can facilitate the passing of information between two processes. These types of constructs can serve as the basis for communicating between agents which can invoke different procedures for different messages and wait until receiving and or sending messages. We will see another example of using a blocking queue later in this chapter.
6.4.4 Allowing Entities to Wait for a Process
In the previous three examples, we saw how we can use a hold queue, a signal, and a blocking queue within a process description. In the case of the blocking queue, we saw how two processes communicated. In this next simple example, we also see how two processes can coordinate their flow via the use of the waitFor(process: KSLProcess)
suspending function. The purpose of the waitFor(process: KSLProcess)
suspending function is to allow one entity to start another process and have the entity that starts the process wait until the newly activated process is completed. The following code indicates the signature of the waitFor(process: KSLProcess)
suspending function.
/** Causes the current process to suspend until the specified process has run to completion.
* This is like run blocking. It activates the specified process and then waits for it
* to complete before proceeding.
*
* @param process the process to start for an entity
* @param timeUntilActivation the time until the start the process
* @param priority the priority associated with the event to start the process
*/
suspend fun waitFor(
process: KSLProcess,
timeUntilActivation: Double = 0.0,
priority: Int = KSLEvent.DEFAULT_PRIORITY,
suspensionName: String? = null
)
The waitFor(process: KSLProcess)
suspending function activates the named process and suspends the current process until the activated process completes. Then, the suspending process is resumed. Let’s take a look at a simple example. Most of this class is simply to define the two interacting processes.
Example 6.6 (Illustrating Waiting for a Process) This example illustrates how you can use one process to start another process. In addition, the process that starts the secondary process will wait until the secondary process completes before continuing.
class WaitForProcessExample(parent: ModelElement) : ProcessModel(parent, null) {
private val worker: ResourceWithQ = ResourceWithQ(this, "worker", 1)
private val tba = RandomVariable(this, ExponentialRV(6.0, 1), "Arrival RV")
private val st = RandomVariable(this, ExponentialRV(3.0, 2), "Service RV")
private val wip = TWResponse(this, "${name}:WIP")
private val tip = Response(this, "${name}:TimeInSystem")
private val arrivals = Arrivals()
private val total = 1
private var n = 1
private inner class Customer : Entity() {
val simpleProcess: KSLProcess = process {
println("\t $time > starting simple process for entity: ${this@Customer.name}")
wip.increment()
timeStamp = time
use(worker, delayDuration = st)
tip.value = time - timeStamp
wip.decrement()
println("\t $time > completed simple process for entity: ${this@Customer.name}")
}
val wfp = process {
val c = Customer()
println("$time > before waitFor simple process for entity: ${this@Customer.name}")
waitFor(c.simpleProcess)
println("$time > after waitFor simple process for entity: ${this@Customer.name}")
}
}
override fun initialize() {
arrivals.schedule(tba)
}
private inner class Arrivals : EventAction<Nothing>() {
override fun action(event: KSLEvent<Nothing>) {
if (n <= total) {
val c = Customer()
println("$time > activating the waitFor process for entity: ${c.name}")
activate(c.wfp)
schedule(tba)
n++
}
}
}
}
The Customer
class is very similar to previous examples of a simple queueing situation. However, notice the use of the use()
function. Because the combination of seize-delay-release is so common, the KSL provides the use()
function to combine these into a convenient suspending function. It is illustrative to see the implementation:
suspend fun use(
resource: ResourceWithQ,
amountNeeded: Int = 1,
seizePriority: Int = PRIORITY,
delayDuration: GetValueIfc,
delayPriority: Int = PRIORITY,
) {
val a = seize(resource, amountNeeded, seizePriority)
delay(delayDuration, delayPriority)
release(a)
}
Getting back to Example 6.6, the code defines a process called WaitForAnotherProcess.
The sole purpose of this process is to create an instance of the customer, activate its simple process, and wait for it to complete. The output from activating one instance of the wait for another process is as follows:
0.8149947795247992 > activating the waitFor process for entity: ID_1
0.8149947795247992 > before waitFor simple process for entity: ID_1
0.8149947795247992 > starting simple process for entity: ID_2
5.091121672376351 > completed simple process for entity: ID_2
5.091121672376351 > after waitFor simple process for entity: ID_1
We see that the activation of entity ID_1 occurs at time 0.81, when it then subsequently activates entity ID_2’s simple process. Entity ID_1 then suspends while entity ID_2 executes its simple process, which completes at time 5.09. Then, entity ID_1’s process is allowed to complete. Thus, we see that it is easy to activate separate processes and to coordinate their completion.
6.4.5 Process Interaction
In this section, we will explore how processes associated with two (or more) different entities can interact during their execution by directly suspending and resuming each other. The previous process constructs are built upon the general lower level functionality of suspending the process and then resuming the process. For example, for the wait and signal construct, the entity will wait on a particular signal. During the wait, the entity’s process is suspended. The signal causes the waiting entities to resume their process from the wait for signal suspension point. The KSL implements the previously described suspension functions by using the low-level suspend()
function. Once an entity’s process is suspended, it needs to be resumed. The entity’s resumeProcess()
function provides this capability. The following example illustrates how to facilitate the direct suspension and resumption of processes.
Example 6.7 (Direct Process Interaction) Consider modeling the interaction between a soccer mom and her daughter. The soccer mom has a daughter who plays forward on a soccer team. The game day proceeds as follows. First, the mom drives the daughter to the game. The drive takes 30 minutes. After arriving to the field, the daughter exits the mini-van, which takes 2 minutes. After the daughter exits the van, the mom departs to run some errands, meanwhile the daughter plays soccer. The mom’s errands take approximately 45 minutes. The soccer game takes approximately 60 minutes. If the mom returns from the errands before the game ends, the mom waits patiently to pick up her daughter to go home. If the game ends, before the mom returns, the daughter waits patiently for the mom to return. Once the mother and daughter are together, the daughter loads up her soccer gear and enters the van. This loading time takes 2 minutes. After all is loaded, the happy mom and daughter drive home, which takes 30 minutes.
We can build a process model for this situation and use instances of the Suspension
class within the implementation. The key to implementing this situation is to capture the suspensions and to share the references of the entities. Let’s start with the mom’s process. The following code shows the mom’s process with extensive print statements added to illustrate the interaction.
private inner class Mother : Entity() {
val daughterExiting: Suspension = Suspension(name = "Suspend for daughter to exit van")
val daughterPlaying: Suspension = Suspension(name = "Suspend for daughter playing")
val daughterLoading: Suspension = Suspension(name = "Suspend for daughter entering van")
val momProcess = process {
println("$time> starting mom = ${this@Mother.name}")
println("$time> mom = ${this@Mother.name} driving to game")
delay(30.0)
println("$time> mom = ${this@Mother.name} arrived at game")
val daughter = Daughter(this@Mother)
activate(daughter.daughterProcess)
println("$time> mom = ${this@Mother.name} suspending for daughter to exit van")
//suspend mom's process
suspendFor(daughterExiting)
println("$time> mom = ${this@Mother.name} running errands...")
delay(45.0)
println("$time> mom = ${this@Mother.name} completed errands")
if (daughter.motherShopping.isSuspended){
println("$time> mom, ${this@Mother.name}, mom resuming daughter done playing after errands")
resume(daughter.motherShopping)
} else {
println("$time> mom, ${this@Mother.name}, mom suspending because daughter is still playing")
suspendFor(daughterPlaying)
}
suspendFor(daughterLoading)
println("$time> mom = ${this@Mother.name} driving home")
delay(30.0)
println("$time> mom = ${this@Mother.name} arrived home")
}
}
In this modeling, it is important to realize the reason for suspensions. The soccer mom will need three suspensions: 1) suspending while the daughter exits the vehicle, 2) suspending if the daughter is still playing, or 3) suspending while the daughter is entering the van. The three instances of the Suspension
class represent these three possibilities and also provide shared state information.
The next concept to grasp is that the mom’s process activates the daughter’s process. The mom’s process creates an instance of the daughter and then activates the daughter’s process. The mom’s process captures a reference to the Daughter
in the daughter
variable. The activation of a process schedules an event to occur. In this case, the event is scheduled for the current time. Note that the daughter’s process starts after the mom completes the delay for driving to the field.
Then, the mom suspends using the suspendFor()
function, specifying the appropriate Suspension
instance. The suspendFor()
function takes in the suspension that labels the suspension point. Now, let’s look at the daughter’s process.
When the mom process activates the daughter, the daughter starts the delay to exit the van. After exiting the van, the daughter tells the mom process via the resume(mother.daughterExiting)
function to resume. Recall, that the mom process immediately suspended while the daughter exited the van. The critical item to note is that a reference to the Mom
entity is required to create the daughter instance and the suspensions are public properties of the mother class. Thus, the daughter has a reference to the mom instance within the daughter’s process routine.
In the following code, the daughter has her own suspension possibility via the motherShopping
Suspension.
private inner class Daughter(val mother: Mother) : Entity() {
val motherShopping: Suspension = Suspension("Suspend for mother shopping")
val daughterProcess = process {
println("$time> starting daughter ${this@Daughter.name}")
println("$time> daughter, ${this@Daughter.name}, exiting the van")
delay(2.0)
println("$time> daughter, ${this@Daughter.name}, exited the van")
// resume mom process
println("$time> daughter, ${this@Daughter.name}, resuming mom")
resume(mother.daughterExiting)
println("$time> daughter, ${this@Daughter.name}, starting playing")
// delay(30.0)
delay(60.0)
println("$time> daughter, ${this@Daughter.name}, finished playing")
// suspend if mom isn't here
if (mother.daughterPlaying.isSuspended){
// mom's errand was completed and mom suspended because daughter was playing
resume(mother.daughterPlaying)
} else {
println("$time> daughter, ${this@Daughter.name}, mom errands not completed suspending")
suspendFor(motherShopping)
}
println("$time> daughter, ${this@Daughter.name}, entering van")
delay(2.0)
println("$time> daughter, ${this@Daughter.name}, entered van")
// resume mom process
println("$time> daughter, ${this@Daughter.name}, entered van, resuming mom")
resume(mother.daughterLoading)
}
}
After playing the daughter uses the daughterPlaying
Suspension of the mother to determine if the mother is suspended because her errands were completed before the daughter completed playing soccer. If the mother is not suspended for the daughter’s playing, then the daughter suspends while the mother completes shopping.
Now, let’s look at the rest of the mom’s process. We see that after suspending to let the daughter exit the van, the mom will run her errands. Since the mom could return early, she checks if the daughter is suspended waiting for her to complete shopping, and if so, the mother resumes the daughter (so that the daughter can start loading). If the daughter is not suspended for the mom’s shopping, she is still playing. So, the mom will suspend until the daughter completes playing.
val momProcess = process {
println("$time> starting mom = ${this@Mother.name}")
println("$time> mom = ${this@Mother.name} driving to game")
delay(30.0)
println("$time> mom = ${this@Mother.name} arrived at game")
val daughter = Daughter(this@Mother)
activate(daughter.daughterProcess)
println("$time> mom = ${this@Mother.name} suspending for daughter to exit van")
//suspend mom's process
suspendFor(daughterExiting)
println("$time> mom = ${this@Mother.name} running errands...")
delay(45.0)
println("$time> mom = ${this@Mother.name} completed errands")
if (daughter.motherShopping.isSuspended){
println("$time> mom, ${this@Mother.name}, mom resuming daughter done playing after errands")
resume(daughter.motherShopping)
} else {
println("$time> mom, ${this@Mother.name}, mom suspending because daughter is still playing")
suspendFor(daughterPlaying)
}
suspendFor(daughterLoading)
println("$time> mom = ${this@Mother.name} driving home")
delay(30.0)
println("$time> mom = ${this@Mother.name} arrived home")
}
The mother will always suspend for the daughter loading. After being resumed by the daughter to indicate that the loading is complete, the mom delays for the drive home. The following output illustrates what happens for the case of the mother’s errand time being less than the playing time of the daughter.
0.0> starting mom = ID_1
0.0> mom = ID_1 driving to game
30.0> mom = ID_1 arrived at game
30.0> mom = ID_1 suspending for daughter to exit van
30.0> starting daughter ID_2
30.0> daughter, ID_2, exiting the van
32.0> daughter, ID_2, exited the van
32.0> daughter, ID_2, resuming mom
32.0> daughter, ID_2, starting playing
32.0> mom = ID_1 running errands...
77.0> mom = ID_1 completed errands
77.0> mom, ID_1, mom suspending because daughter is still playing
92.0> daughter, ID_2, finished playing
92.0> daughter, ID_2, entering van
94.0> daughter, ID_2, entered van
94.0> daughter, ID_2, entered van, resuming mom
94.0> mom = ID_1 driving home
124.0> mom = ID_1 arrived home
We see in this output that the mom suspends at time 30, while the daughter exits the van. The daughter starts playing and the mom starts her errands at time 32. The mom completes her errands at time 77 and suspends until the daughter is done playing at time 92. At which time, the daughter enters the van at time 94. They both drive off “together” starting at time 94.
We encourage the interested reader to re-run the code after changing the playing time to a smaller value, for example, 30 minutes. Then, you will see that the daughter suspends until the mother completes her errands. Thus, by carefully suspending and resuming processes, we can coordinate their interaction as they proceed through time. This is the essence of the process interaction approach to simulation model representation. However, this low level coordination requires special attention to shared state and a deep understanding of how the processes interact, which can be error prone.
This same interaction can be achieved using other KSL constructs. Specifically, the HoldQueue
and Signal
constructs can also be used. Notice that when using the Suspension
class, there were no waiting statistics collected when the processes wait for each other to complete. If you need waiting statistics, then using the HoldQueue
or the Signal
class can be useful.
The following code illustrates the use of the HoldQueue
class. Notice that there are four hold queues defined. These represent the suspension points from the previous example. Just as before, the mother activates the daughter and then suspends via the hold()
function. The mother uses the shared instances of the hold queues to check if the daughter is waiting in the waitForMomToShopQ
hold queue. If so, the mother removes the daughter from the queue and then resumes the daughter’s process. the mother will suspend (if necessary) for the daughter to complete playing via the hold(waitForDaughterToPlayQ)
call. In addition, the mother will hold for the daughter to enter the van.
class SoccerMomViaHoldQ(
parent: ModelElement,
name: String? = null
) : ProcessModel(parent, name) {
private val waitForDaughterToExitQ = HoldQueue(this, name = "WaitForDaughterToExitQ")
private val waitForDaughterToPlayQ = HoldQueue(this, name = "WaitForDaughterToPlayQ")
private val waitForDaughterToLoadQ = HoldQueue(this, name = "WaitForDaughterToLoadQ")
private val waitForMomToShopQ = HoldQueue(this, name = "WaitForMomToShopQ")
override fun initialize() {
val m = Mother()
activate(m.momProcess)
}
private inner class Mother : Entity() {
val momProcess = process {
println("$time> starting mom = ${this@Mother.name}")
println("$time> mom = ${this@Mother.name} driving to game")
delay(30.0)
println("$time> mom = ${this@Mother.name} arrived at game")
val daughter = Daughter(this@Mother)
activate(daughter.daughterProcess)
println("$time> mom = ${this@Mother.name} suspending for daughter to exit van")
//suspend mom's process
hold(waitForDaughterToExitQ)
println("$time> mom = ${this@Mother.name} running errands...")
delay(45.0)
println("$time> mom = ${this@Mother.name} completed errands")
if (waitForMomToShopQ.contains(daughter)){
println("$time> mom, ${this@Mother.name}, mom resuming daughter done playing after errands")
waitForMomToShopQ.removeAndResume(daughter)
} else {
println("$time> mom, ${this@Mother.name}, mom suspending because daughter is still playing")
hold(waitForDaughterToPlayQ)
}
hold(waitForDaughterToLoadQ)
println("$time> mom = ${this@Mother.name} driving home")
delay(30.0)
println("$time> mom = ${this@Mother.name} arrived home")
}
}
The daughter’s process is very similar to the previous example code. Notice that the shared references to the hold queues are used to check if the mother is waiting.
private inner class Daughter(val mother: Mother) : Entity() {
val daughterProcess = process {
println("$time> starting daughter ${this@Daughter.name}")
println("$time> daughter, ${this@Daughter.name}, exiting the van")
delay(2.0)
println("$time> daughter, ${this@Daughter.name}, exited the van")
// resume mom process
println("$time> daughter, ${this@Daughter.name}, resuming mom")
waitForDaughterToExitQ.removeAndResume(mother)
println("$time> daughter, ${this@Daughter.name}, starting playing")
delay(30.0)
// delay(60.0)
println("$time> daughter, ${this@Daughter.name}, finished playing")
// suspend if mom isn't here
if (waitForDaughterToPlayQ.contains(mother)){
// mom's errand was completed and mom suspended because daughter was playing
waitForDaughterToPlayQ.removeAndResume(mother)
} else {
println("$time> daughter, ${this@Daughter.name}, mom errands not completed suspending")
hold(waitForMomToShopQ)
}
println("$time> daughter, ${this@Daughter.name}, entering van")
delay(2.0)
println("$time> daughter, ${this@Daughter.name}, entered van")
// resume mom process
println("$time> daughter, ${this@Daughter.name}, entered van, resuming mom")
waitForDaughterToLoadQ.removeAndResume(mother)
}
}
Since the implementation using the Signal
class is very similar, its use is left as an exercise for the reader.
The KSL has one other concept that facilitates process interaction: blockages. A blockage is like a semaphore or lock on a portion of a suspending function. Think of a blockage as a gate that prevents another process from proceeding with its own process until another process has proceeded through the marked portion of code.
A blockage can be used to block (suspend) other entities while they wait for the blockage to be cleared. The user can mark process code with the start of a blockage and a subsequent end of the blockage. While the entity that creates the blockage is within the blocking code, other entities can be made to wait until the blockage is cleared. Only the entity that creates the blockage can start and clear it. A started blockage must be cleared before the end of the process routine that contains it; otherwise, an exception will occur. Thus, blockages that are started must always be cleared. The primary purpose of this construct is to facilitate process interaction between entities. Using blockages should be preferred over the use of Suspension
instances. The following code illustrates the soccer mom and daughter interaction using blockages.
For the purposes of clarity of presentation, the print statements have been removed from the example code. The exercises ask the reader to show via print statements that the implementation using blockages results in the same interaction. Let’s start with the mother’s process. Since the daughter might have to wait for the mom to finish shopping, we will define a blockage to cover this activity.
private inner class Mom : Entity() {
val shopping: Blockage = Blockage("shopping")
val momProcess = process {
delay(30.0)
val daughter = Daughter(this@Mom)
activate(daughter.daughterProcess)
waitFor(daughter.unloading)
startBlockage(shopping)
delay(45.0)
clearBlockage(shopping)
waitFor(daughter.playing)
waitFor(daughter.loading)
delay(30.0)
println("$time> mom = ${this@Mom.name} arrived home")
}
}
Again, the mother creates and activates the daughter. The mother uses the waitFor(blockage: Blockage)
suspending function to wait for the daughter to complete unloading. To see how this works, let’s review the daughter’s process.
The daughter has three potential blockages to represent unloading, playing, and loading. In the following code, notice how the unloading delay is between the start of a blockage and the clearing of the unloading blockage. This matching startBlockage
and clearBlockage
is like putting up a fence. The waitFor(daughter.unloading)
call within the mother’s process, will check if the blockage is started or cleared. If the blockage is cleared, then the entity immediately continues (does not suspend); however, if the blockage is started, the entity executing the waitFor()
will suspend until the blockage is cleared. Thus, the mom will wait until the daughter is unloaded.
private inner class Daughter(val mom: Mom) : Entity() {
val unloading: Blockage = Blockage("unloading")
val playing: Blockage = Blockage("playing")
val loading: Blockage = Blockage("loading")
val daughterProcess = process {
startBlockage(unloading)
delay(2.0)
clearBlockage(unloading)
startBlockage(playing)
// delay(30.0)
delay(60.0)
clearBlockage(playing)
waitFor(mom.shopping)
startBlockage(loading)
delay(2.0)
clearBlockage(loading)
}
}
Notice the use of the shopping
blockage within the mother’s process. This blockage surrounds the delay for shopping. Now, in the daughter’s process we see the use of the waitFor(mom.shopping)
function call. Again, if the mom is still shopping, the daughter will suspend; however, if the mom has cleared the shopping blockage, the daughter proceeds to the unloading activity. If you need queue statistics, when using blockages, you can still use the Queue
class and enqueue the entity prior to the waitFor
call and remove the entity after the waitFor
call.
IMPORTANT! A blockage can only be started and cleared by the entity that instantiates it. In addition, a blockage that has been started must be cleared before the end of the process within which it appears. A common error in the usage of blockages would be to not clear the blockage.
As illustrated in the previous example, using a blockage around an activity is a common use case. Because of this, the KSL provides a special type of blockage just for activities. Here is the code that uses BlockingActivity
instances.
private inner class Mom : Entity() {
val shopping: BlockingActivity = BlockingActivity(45.0)
val momProcess = process {
delay(30.0)
val daughter = Daughter(this@Mom)
activate(daughter.daughterProcess)
waitFor(daughter.unloading)
perform(shopping)
waitFor(daughter.playing)
waitFor(daughter.loading)
delay(30.0)
}
}
Notice the use of the new perform()
suspending function. The perform()
function essentially wraps a delay with a blockage. This ensures that you do not forget to clear the blockage. The daughter’s process is also simplified as follows.
private inner class Daughter(val mom: Mom) : Entity() {
val unloading: BlockingActivity = BlockingActivity(2.0)
val playing: BlockingActivity = BlockingActivity(60.0)
val loading: BlockingActivity = BlockingActivity(2.0)
val daughterProcess = process {
perform(unloading)
perform(playing)
waitFor(mom.shopping)
perform(loading)
}
}
Besides blockages for activities, the KSL provides blockages for the following common situations:
BlockingResourceUsage
- Wraps a blockage around theuse()
suspending function when using a resource or resource with queue during an activity.BlockingResourcePoolUsage
- Wraps a blockage around theuse()
suspending function when using a resource pool or resource pool with queue during an activity.BlockingMovement
- Wraps a blockage around the use of themove()
suspending function. The movement of entities will be discussed in a subsequent chapter.
In the next section, we will develop a more realistically sized process model for a STEM Career Mixer involving students and recruiters.