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.
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.
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 to 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("SimpleProcess", addToSequence = false) {
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("WaitForAnotherProcess", addToSequence = false) {
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 = KSLEvent.DEFAULT_PRIORITY,
delayDuration: GetValueIfc,
delayPriority: Int = KSLEvent.DEFAULT_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 final example, we will explore how processes associated with two 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 the suspend()
and resumeProcess()
functions within the implementation. The key to implementing this situation is to have variables that indicate the current state of the entity 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 Mom : Entity() {
var errandsCompleted = false
val momProcess = process {
println("$time> starting mom = ${this@Mom.name}")
println("$time> mom = ${this@Mom.name} driving to game")
delay(30.0)
println("$time> mom = ${this@Mom.name} arrived at game")
val daughter = Daughter(this@Mom)
activate(daughter.daughterProcess)
println("$time> mom = ${this@Mom.name} suspending for daughter to exit van")
suspend("mom suspended for daughter to exit van")
.
.
}
}
The first 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 immediately suspends using the suspend()
function. The suspend()
function takes in an optional string parameter 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 resumeProcess()
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. Thus, the daughter has a reference to the mom instance in the process routine.
In the following code, the daughter indicates that she is playing by changing the value of the isPlaying
property to true. This property can be used by the mom to check if the daughter is still playing after the mom returns from running the errands.
private inner class Daughter(val mom: Mom) : Entity() {
var isPlaying = false
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")
println("$time> daughter, ${this@Daughter.name}, resuming mom")
mom.resumeProcess()
println("$time> daughter, ${this@Daughter.name}, starting playing")
isPlaying = true
// delay(30.0)
delay(60.0)
isPlaying = false
println("$time> daughter, ${this@Daughter.name}, finished playing")
if (!mom.errandsCompleted){
println("$time> daughter, ${this@Daughter.name}, mom errands not completed suspending")
suspend("daughter waiting on mom to complete errand")
}
println("$time> daughter, ${this@Daughter.name}, entering van")
delay(2.0)
println("$time> daughter, ${this@Daughter.name}, entered van")
println("$time> daughter, ${this@Daughter.name}, entered van, resuming mom")
mom.resumeProcess()
}
}
After playing the daughter can use the errandsCompleted
property of the mom instance to suspend if her mother has not returned from the errands.
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 and change the property errandsCompleted
to true when she is done. Since the mom could return early, she checks if the daughter is playing, and if so, suspends. If the daughter is not playing, then the mom tells the daughter’s process to resume, and then the mom process immediately suspends to allow the daughter to enter the van.
private inner class Mom : Entity() {
var errandsCompleted = false
val momProcess = process {
println("$time> starting mom = ${this@Mom.name}")
println("$time> mom = ${this@Mom.name} driving to game")
delay(30.0)
println("$time> mom = ${this@Mom.name} arrived at game")
val daughter = Daughter(this@Mom)
activate(daughter.daughterProcess)
println("$time> mom = ${this@Mom.name} suspending for daughter to exit van")
suspend("mom suspended for daughter to exit van")
println("$time> mom = ${this@Mom.name} running errands...")
delay(45.0)
println("$time> mom = ${this@Mom.name} completed errands")
errandsCompleted = true
if (daughter.isPlaying){
println("$time> mom, ${this@Mom.name}, mom suspending because daughter is still playing")
suspend("mom suspended for daughter playing")
} else {
println("$time> mom, ${this@Mom.name}, mom resuming daughter done playing after errands")
daughter.resumeProcess()
suspend("mom suspended for daughter entering van")
}
println("$time> mom = ${this@Mom.name} driving home")
delay(30.0)
println("$time> mom = ${this@Mom.name} arrived home")
}
}
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.
In the next section, we will develop a more realistically sized process model for a STEM Career Mixer involving students and recruiters.