Advanced Simpy
Advanced Simpy
Norm Matloff
February 29, 2008
c
2006-2008,
N.S. Matloff
Contents
1
Overview
2.1
2.2
2.3
Job Interruption
3.1
3.2
Interthread Synchronization
12
4.1
4.2
4.3
4.4
15
5.1
5.2
Overview
In this document we present several advanced features of the SimPy language. These will make your SimPy
programming more convenient and enjoyable. In small programs, use of some of these features will produce a modest but worthwhile reduction on programming effort and increase in program clarity. In large
programs, the savings add up, and can make a very significant improvement.
In many simulation programs, a thread is waiting for one of two events; whichever occurs first will trigger
a resumption of execution of the thread. The thread will typically want to ignore the other, later-occurring
event. We can use SimPys cancel() function to cancel the later event.
2.1
An example of this is in the program TimeOut.py. The model consists of a network node which transmits
but also sets a timeout period, as follows: After sending the message out onto the network, the node waits
for an acknowledgement from the recipient. If an acknowledgement does not arrive within a certain specified
period of time, it is assumed that the message was lost, and it will be sent again. We wish to determine the
percentage of attempted transmissions which result in timeouts.
The timeout period is assumed to be 0.5, and acknowledgement time is assumed to be exponentially distributed with mean 1.0. Here is the code:
1
#!/usr/bin/env python
2
3
4
5
6
7
8
9
10
#
#
#
#
#
#
#
#
11
12
13
14
15
16
17
18
19
#
#
#
20
21
22
23
24
25
26
27
#
#
#
#
#
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class Node(Process):
def __init__(self):
Process.__init__(self)
self.NMsgs = 0 # number of messages sent
self.NTimeOuts = 0 # number of timeouts which have occurred
# ReactivatedCode will be 1 if timeout occurred, 2 ACK if received
self.ReactivatedCode = None
def Run(self):
while 1:
self.NMsgs += 1
# set up the timeout
G.TO = TimeOut()
activate(G.TO,G.TO.Run())
# set up message send/ACK
G.ACK = Acknowledge()
activate(G.ACK,G.ACK.Run())
yield passivate,self
if self.ReactivatedCode == 1:
self.NTimeOuts += 1
self.ReactivatedCode = None
52
53
54
55
56
57
58
59
60
61
class TimeOut(Process):
TOPeriod = 0.5
def __init__(self):
Process.__init__(self)
def Run(self):
yield hold,self,TimeOut.TOPeriod
G.Nd.ReactivatedCode = 1
reactivate(G.Nd)
self.cancel(G.ACK)
62
63
64
65
66
67
68
69
70
71
class Acknowledge(Process):
ACKRate = 1/1.0
def __init__(self):
Process.__init__(self)
def Run(self):
yield hold,self,G.Rnd.expovariate(Acknowledge.ACKRate)
G.Nd.ReactivatedCode = 2
reactivate(G.Nd)
self.cancel(G.TO)
72
73
74
75
class G: # globals
Rnd = Random(12345)
Nd = Node()
76
77
78
79
80
81
def main():
initialize()
activate(G.Nd,G.Nd.Run())
simulate(until=10000.0)
print the percentage of timeouts was, float(G.Nd.NTimeOuts)/G.Nd.NMsgs
82
83
if __name__ == __main__:
main()
The main driver here is a class Node, whose PEM code includes the lines
1
2
3
4
5
6
7
8
9
10
while 1:
self.NMsgs += 1
G.TO = TimeOut()
activate(G.TO,G.TO.Run())
G.ACK = Acknowledge()
activate(G.ACK,G.ACK.Run())
yield passivate,self
if self.ReactivatedCode == 1:
self.NTimeOuts += 1
self.ReactivatedCode = None
The node creates an object G.TO of our TimeOut class, which will simulate a timeout period, and creates
an object G.ACK of our Acknowledge class to simulate a transmission and acknowledgement. Then the
node passivates itself, allowing G.TO and G.ACK to do their work. One of them will finish first, and then
will call SimPys reactivate() function to wake up the suspended node. The node senses whether it was
a timeout or acknowledgement which woke it up, via the variable ReactivatedCode, and then updates its
timeout count accordingly.
Heres what TimeOut.Run() does:
1
2
3
4
yield hold,self,TimeOut.TOPeriod
G.Nd.ReactivatedCode = 1
reactivate(G.Nd)
self.cancel(G.ACK)
It holds a random timeout time, then sets a flag in Nd to let the latter know that it was the timeout which
occurred first, rather than the acknowledgement. Then it reactivates Nd and cancels ACK. ACK of course
has similar code for handling the case in which the acknowledgement occurs before the timeout.
Note that in our case here, we want the thread to go out of existence when canceled. The cancel() function
does not make that occur. It simply removes the pending events associated with the given thread. The thread
is still there.
However, here the TO and ACK threads will go out of existence anyway, for a somewhat subtle reason:1
Think of what happens when we finish one iteration of the while loop in main(). A new object of type
TimeOut will be created, and then assigned to G.TO. That means that the G.TO no longer points to the old
TimeOut object, and since nothing else points to it either, the Python interpreter will now garbage collect
that old object.
2.2
#!/usr/bin/env python
2
3
# JobBreak.py
4
5
6
7
8
9
10
#
#
#
#
#
#
One machine, which sometimes breaks down. Up time and repair time are
exponentially distributed. There is a continuing supply of jobs
waiting to use the machine, i.e. when one job finishes, another
immediately begins. When a job is interrupted by a breakdown, it
resumes "where it left off" upon repair, with whatever time remaining
that it had before.
11
12
13
14
15
import sys
16
17
18
19
20
class G: # globals
CurrentJob = None
Rnd = Random(12345)
M = None # our one machine
21
22
class Machine(Process):
1
23
24
25
26
27
28
29
30
31
32
33
34
35
36
def __init__(self):
Process.__init__(self)
def Run(self):
while 1:
UpTime = G.Rnd.expovariate(Machine.UpRate)
yield hold,self,UpTime
CJ = G.CurrentJob
self.cancel(CJ)
NewNInts = CJ.NInts + 1
NewTimeLeft = CJ.TimeLeft - (now()-CJ.LatestStart)
RepairTime = G.Rnd.expovariate(Machine.RepairRate)
yield hold,self,RepairTime
G.CurrentJob = Job(CJ.ID,NewTimeLeft,NewNInts,CJ.OrigStart,now())
activate(G.CurrentJob,G.CurrentJob.Run())
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class Job(Process):
ServiceRate = None
NDone = 0 # jobs done so far
TotWait = 0.0 # total wait for those jobs
NNoInts = 0 # jobs done so far that had no interruptions
def __init__(self,ID,TimeLeft,NInts,OrigStart,LatestStart):
Process.__init__(self)
self.ID = ID
self.TimeLeft = TimeLeft # amount of work left for this job
self.NInts = NInts # number of interruptions so far
# time this job originally started
self.OrigStart = OrigStart
# time the latest work period began for this job
self.LatestStart = LatestStart
def Run(self):
yield hold,self,self.TimeLeft
# job done
Job.NDone += 1
Job.TotWait += now() - self.OrigStart
if self.NInts == 0: Job.NNoInts += 1
# start the next job
SrvTm = G.Rnd.expovariate(Job.ServiceRate)
G.CurrentJob = Job(G.CurrentJob.ID+1,SrvTm,0,now(),now())
activate(G.CurrentJob,G.CurrentJob.Run())
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
def main():
Job.ServiceRate = float(sys.argv[1])
Machine.UpRate = float(sys.argv[2])
Machine.RepairRate = float(sys.argv[3])
initialize()
SrvTm = G.Rnd.expovariate(Job.ServiceRate)
G.CurrentJob = Job(0,SrvTm,0,0.0,0.0)
activate(G.CurrentJob,G.CurrentJob.Run())
G.M = Machine()
activate(G.M,G.M.Run())
MaxSimtime = float(sys.argv[4])
simulate(until=MaxSimtime)
print mean wait:, Job.TotWait/Job.NDone
print % of jobs with no interruptions:, \
float(Job.NNoInts)/Job.NDone
78
79
Here we have one machine, with occasional breakdown, but we also keep track of the number of jobs done.
See the comments in the code for details.
Here we have set up a class Job. When a new job starts service, an instance of this class is set up to model
that job. If its service then runs to completion without interruption, fine. But if the machine breaks down in
the midst of service, this instance of the Job class will be discarded, and a new instance will later be created
when this job resumes service after the repair. In other words, each object of the class Job models one job
5
to be done, but it can be either a brand new job or the resumption of an interrupted job.
Lets take a look at Job.Run():
1
2
3
4
5
6
7
yield hold,self,self.TimeLeft
Job.NDone += 1
Job.TotWait += now() - self.OrigStart
if self.NInts == 0: Job.NNoInts += 1
SrvTm = G.Rnd.expovariate(Job.ServiceRate)
G.CurrentJob = Job(G.CurrentJob.ID+1,SrvTm,0,now(),now())
activate(G.CurrentJob,G.CurrentJob.Run())
This looks innocuous enough. We hold for the time it takes to finish the job, then update our totals, and
launch the next job. What is not apparent, though, is that we may actually never reach that second line,
Job.NDone += 1
The reason for this is that the machine may break down before the job finishes. In that case, what we have
set up is that Machine.Run() will cancel the pending job completion event,
self.cancel(CJ)
and then create a new instance of Job which will simulate the processing of the remainder of the interrupted
job (which may get interrupted too):
NewNInts = CJ.NInts + 1
NewTimeLeft = CJ.TimeLeft - (now()-CJ.LatestStart)
...
G.CurrentJob = Job(CJ.ID,NewTimeLeft,NewNInts,CJ.OrigStart,now())
activate(G.CurrentJob,G.CurrentJob.Run())
There are other ways of doing this, in particular by using SimPys interrupt() and interrupted() functions,
but we defer this to Section 3.
2.3
# simulates one cell in a cellular phone network; here all calls are
# local, no handoffs; calls last a random time; if a channel is not
# available when a new call arrives, the oldest one is pre-empted
# usage:
# python Cell2.py ArrRate DurRate NChnls MaxSimTime
# where:
#
#
#
#
#
between arrivals)
DurRate = reciprocal of mean duration of local calls
NChnls = number of channels
MaxSimtime = amount of time to simulate
import sys,random
from SimPy.Simulation import *
from PeriodicSampler import *
class Globals:
Rnd = random.Random(12345)
Debug = False
class Cell:
NChnls = None
NFreeChannels = None
class CellMonClass: # to set up PeriodicSampler
def __init__(self):
self.ChnlMon = Monitor()
def RecordNBusyChnls(self):
return Cell.NChnls - Cell.NFreeChannels
class Call(Process):
DurRate = None # reciprocal of mean call duration
NArrv = 0 # number of calls arrived so far
NPre_empted = 0 # number of calls pre-empted so far
NextID = 0 # for debugging
CurrentCalls = [] # pointers to the currently-active calls
ChnlMon = Monitor() # to monitor number of busy channels
FracMon = Monitor() # to monitor pre-emption fractions
def __init__(self):
Process.__init__(self)
self.ID = Call.NextID
Call.NextID += 1
self.MyStartTime = now()
self.MyFinishTime = None
def Run(self): # simulates one call
Call.NArrv += 1
CallTime = Globals.Rnd.expovariate(Call.DurRate)
self.MyFinishTime = now() + CallTime
if Globals.Debug: self.ShowStatus()
Call.CurrentCalls.append(self)
if Cell.NFreeChannels == 0: # no channels available
Oldest = Call.CurrentCalls.pop(0)
self.cancel(Oldest)
Cell.NFreeChannels += 1 # one channel freed
Call.NPre_empted += 1
FullCallLength = Oldest.MyFinishTime - Oldest.MyStartTime
AmountPre_empted = Oldest.MyFinishTime - now()
Call.FracMon.observe(AmountPre_empted/FullCallLength)
del Oldest
Cell.NFreeChannels -= 1 # grab the channel
Call.ChnlMon.observe(Cell.NChnls-Cell.NFreeChannels)
yield hold,self,CallTime
Cell.NFreeChannels += 1 # release the channel
Call.ChnlMon.observe(Cell.NChnls-Cell.NFreeChannels)
if Call.CurrentCalls != []:
Call.CurrentCalls.remove(self)
return # not needed, but enables a breakpoint here
def ShowStatus(self): # for debugging and program verification
print
print time, now()
print Cell.NFreeChannels, free channels
print ID,self.ID,finish time is,self.MyFinishTime
print current calls:
for CurrCall in Call.CurrentCalls:
print CurrCall.ID,CurrCall.MyFinishTime
print next arrival at,Arrivals.NextArrival
class Arrivals(Process):
ArrRate = None
NextArrival = None # for debugging and program verification
def __init__(self):
Process.__init__(self)
def Run(self):
while 1:
TimeToNextArrival = Globals.Rnd.expovariate(Arrivals.ArrRate)
Arrivals.NextArrival = now() + TimeToNextArrival
yield hold,self,TimeToNextArrival
C = Call()
activate(C,C.Run())
def main():
if debug in sys.argv: Globals.Debug = True
Arrivals.ArrRate = float(sys.argv[1])
Call.DurRate = float(sys.argv[2])
Cell.NChnls = int(sys.argv[3])
Cell.NFreeChannels = Cell.NChnls
initialize()
Arr = Arrivals()
activate(Arr,Arr.Run())
CMC = CellMonClass()
CMC.PrSm = PerSmp(0.1,CMC.ChnlMon,CMC.RecordNBusyChnls)
activate(CMC.PrSm,CMC.PrSm.Run())
MaxSimtime = float(sys.argv[4])
simulate(until=MaxSimtime)
print fraction of pre-empted calls:, Call.NPre_empted/float(Call.NArrv)
print average fraction cut among pre-empted:,Call.FracMon.mean()
print mean number of active channels, Method I:,Call.ChnlMon.timeAverage()
print mean number of active channels, Method II:,CMC.ChnlMon.mean()
if __name__ == __main__: main()
Job Interruption
SimPy allows one thread to interrupt another, which can be very useful.
3.1
In Section 2.2 we had a program JobBreak.py, which modeled a machine with breakdown on which we
collected job time data. We presented that program as an example of cancel(). However, it is much more
easily handeled via the function interrupt(). Here is a new version of the program using that function:
1
#!/usr/bin/env python
2
3
# JobBreakInt.py:
4
5
6
7
8
9
10
#
#
#
#
#
#
One machine, which sometimes breaks down. Up time and repair time are
exponentially distributed. There is a continuing supply of jobs
waiting to use the machine, i.e. when one job finishes, the next
begins. When a job is interrupted by a breakdown, it resumes "where
it left off" upon repair, with whatever time remaining that it had
before.
11
12
13
14
15
import sys
16
17
18
19
20
class G: # globals
CurrentJob = None
Rnd = Random(12345)
M = None # our one machine
21
22
23
24
25
26
27
28
29
30
31
32
33
class Machine(Process):
def __init__(self):
Process.__init__(self)
def Run(self):
from SimPy.Simulation import _e
while 1:
UpTime = G.Rnd.expovariate(Machine.UpRate)
yield hold,self,UpTime
self.interrupt(G.CurrentJob)
RepairTime = G.Rnd.expovariate(Machine.RepairRate)
yield hold,self,RepairTime
reactivate(G.CurrentJob)
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
class Job(Process):
ServiceRate = None
NDone = 0 # jobs done so far
TotWait = 0.0 # total wait for those jobs
NNoInts = 0 # jobs done so far that had no interruptions
NextID = 0
def __init__(self):
Process.__init__(self)
self.ID = Job.NextID
Job.NextID += 1
# amount of work left for this job
self.TimeLeft = G.Rnd.expovariate(Job.ServiceRate)
self.NInts = 0 # number of interruptions so far
# time this job originally started
self.OrigStart = now()
# time the latest work period began for this job
self.LatestStart = now()
def Run(self):
from SimPy.Simulation import _e
while True:
yield hold,self,self.TimeLeft
# did the job run to completion?
if not self.interrupted(): break
self.NInts += 1
self.TimeLeft -= now() - self.LatestStart
yield passivate,self # wait for repair
self.LatestStart = now()
Job.NDone += 1
Job.TotWait += now() - self.OrigStart
if self.NInts == 0: Job.NNoInts += 1
# start the next job
G.CurrentJob = Job()
activate(G.CurrentJob,G.CurrentJob.Run())
68
69
70
71
72
73
74
75
76
77
78
79
80
81
def main():
Job.ServiceRate = float(sys.argv[1])
Machine.UpRate = float(sys.argv[2])
Machine.RepairRate = float(sys.argv[3])
initialize()
G.CurrentJob = Job()
activate(G.CurrentJob,G.CurrentJob.Run())
G.M = Machine()
activate(G.M,G.M.Run())
MaxSimtime = float(sys.argv[4])
simulate(until=MaxSimtime)
print mean wait:, Job.TotWait/Job.NDone
print % of jobs with no interruptions:, \
float(Job.NNoInts)/Job.NDone
82
83
84
A call to interrupt() cancels the pending yield hold operation of its victim, i.e. the thread designated
in the argument. 2 A new artificial event will be created for the victim, with event time being the current
simulated time, now(). The caller does not lose control of the CPU, and continues to execute, but when it
hits its next yield statement (or passivate() etc.) and thus loses control of the CPU, the victim will probably
be next to run, as its (new, artificial) event time will be the current time.
In our case here, at the time
self.interrupt(G.CurrentJob)
is executed by the Machine thread, the current job is in the midst of being serviced. The call interrupts
that service, to reflect the fact that the machine has broken down. At this point, the current jobs event is
canceled, with the artificial event being created as above. The current jobs thread wont run yet, and the
Machine thread will continue. But when the latter reaches the line
yield hold,self,RepairTime
the Machine thread loses control of the CPU and the current jobs thread runs. The latter executes
if not self.interrupted(): break
self.NInts += 1
self.TimeLeft -= now() - self.LatestStart
yield passivate,self # wait for repair
The interruption will be sensed by self.interrupted() returning True. The job thread will then do the proper
bookkeeping, and then passivate itself, waiting for the machine to come back up. When the latter event
occurs, the machines thread executes
reactivate(G.CurrentJob)
The function interrupt() should not be called unless the thread to be interrupted is in the midst of yield hold.
10
1
2
3
4
5
6
7
8
9
10
11
while True:
yield hold,self,self.TimeLeft
# did the job run to completion?
if not self.interrupted(): break
self.NInts += 1
self.TimeLeft -= now() - self.LatestStart
yield passivate,self # wait for repair
self.LatestStart = now()
Job.NDone += 1
Job.TotWait += now() - self.OrigStart
...
In the jobs final cycle (which could be its first), the yield hold will not be interrupted. In this case the call
to interrupted() will inform the thread that it had not been interrupted. The loop will be exited, the final
bookkeeping for this job will be done, and the next job will be started.
By the way, we did not have to have our instance variable TimeLeft in Job. SimPys Process class has
its own built-in instance variable interruptLeft which records how much time in the yield hold had been
remaining at the time of the interruption.
3.2
Use of interrupts makes our old network node acknowledgement/timeout program TimeOut.py in Section
2.1 considerably simpler:
1
#!/usr/bin/env python
2
3
# TimeOutInt.py
4
5
6
7
8
9
10
#
#
#
#
#
#
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Node(Process):
def __init__(self):
Process.__init__(self)
self.NMsgs = 0 # number of messages sent
self.NTimeOuts = 0 # number of timeouts which have occurred
def Run(self):
from SimPy.Simulation import _e
while 1:
self.NMsgs += 1
# set up the timeout
G.TO = TimeOut()
activate(G.TO,G.TO.Run())
# wait for ACK, but could be timeout
yield hold,self,G.Rnd.expovariate(1.0)
if self.interrupted():
self.NTimeOuts += 1
else: self.cancel(G.TO)
35
36
class TimeOut(Process):
11
TOPeriod = 0.5
def __init__(self):
Process.__init__(self)
def Run(self):
from SimPy.Simulation import _e
yield hold,self,TimeOut.TOPeriod
self.interrupt(G.Nd)
37
38
39
40
41
42
43
44
45
46
47
class G: # globals
Rnd = Random(12345)
Nd = Node()
48
49
50
51
52
53
def main():
initialize()
activate(G.Nd,G.Nd.Run())
simulate(until=10000.0)
print the percentage of timeouts was, float(G.Nd.NTimeOuts)/G.Nd.NMsgs
54
55
if __name__ == __main__:
main()
Use of interrupts allowed us to entirely eliminate our old ACK class. Moreover, the code looks more natural
now, as a timeout could be thought of as interrupting the node.
Interthread Synchronization
In our introductory SimPy document, in cases in which one thread needed to wait for some other thread to
take some action,3 we made use of passivate() and reactivate(). Those can be used in general, but more
advanced constructs would make our lives easier.
For example, suppose many threads are waiting for the same action to occur. The thread which triggered
that action would then have to call reactivate() on all of them. Among other things, this would mean we
would have to have code which kept track of which threads were waiting. We could do that, but it would be
nicer if we didnt have to.
In fact, actions like yield waitevent alleviate us of that burden. This makes our code easier to write and
maintain, and easier to read.
4.1
Below is an example, again modeling a machine repair situation. It is similar to MachRep3.py from our
introductory document, but with R machines instead of two, and a policy that the repairperson is called if
the number of operating machines falls below K.
1
#!/usr/bin/env python
2
3
# MachRep4.py
4
5
6
7
Ive used the word action here rather than event, as the latter term refers to items in SimPys internal event list, generated by
yield hold operations. But this wont completely remove the confusion, as the SimPy keyword waitevent will be introduced below.
But again, that term will refer to what Im describing as actions here. The official SimPy term is a SimEvent.
12
8
9
10
# summoned when fewer than K of the machines are up, and reaches the
# site after a negligible amount of time. He keeps repairing machines
# until there are none that need it, then leaves.
11
12
# usage:
13
14
15
16
17
18
19
20
21
class G: # globals
Rnd = Random(12345)
RepairPerson = Resource(1)
RepairPersonOnSite = False
RPArrive = SimEvent()
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class MachineClass(Process):
MachineList = [] # list of all objects of this class
UpRate = None # reciprocal of mean up time
RepairRate = None # reciprocal of mean repair time
R = None # number of machines
K = None # threshold for summoning the repairperson
TotalUpTime = 0.0 # total up time for all machines
NextID = 0 # next available ID number for MachineClass objects
NUp = 0 # number of machines currently up
# create an event to signal arrival of repairperson
def __init__(self):
Process.__init__(self)
self.StartUpTime = None # time the current up period started
self.ID = MachineClass.NextID
# ID for this MachineClass object
MachineClass.NextID += 1
MachineClass.MachineList.append(self)
MachineClass.NUp += 1 # start in up mode
def Run(self):
from SimPy.Simulation import _e
while 1:
self.StartUpTime = now()
yield hold,self,G.Rnd.expovariate(MachineClass.UpRate)
MachineClass.TotalUpTime += now() - self.StartUpTime
MachineClass.NUp -= 1
# if the repairperson is already onsite, just request him;
# otherwise, check whether fewer than K machines are up
if not G.RepairPersonOnSite:
if MachineClass.NUp < MachineClass.K:
G.RPArrive.signal()
G.RepairPersonOnSite = True
else: yield waitevent,self,G.RPArrive
yield request,self,G.RepairPerson
yield hold,self,G.Rnd.expovariate(MachineClass.RepairRate)
MachineClass.NUp += 1
# if no more machines waiting for repair, dismiss repairperson
if G.RepairPerson.waitQ == []:
G.RepairPersonOnSite = False
yield release,self,G.RepairPerson
61
62
63
64
65
66
67
68
69
70
71
72
73
74
def main():
initialize()
MachineClass.R = int(sys.argv[1])
MachineClass.UpRate = float(sys.argv[2])
MachineClass.RepairRate = float(sys.argv[3])
MachineClass.K = int(sys.argv[4])
for I in range(MachineClass.R):
M = MachineClass()
activate(M,M.Run())
MaxSimtime = float(sys.argv[5])
simulate(until=MaxSimtime)
print proportion of up time was, \
MachineClass.TotalUpTime/(MachineClass.R*MaxSimtime)
75
13
76
if __name__ == __main__:
main()
We also set up a variable RepairPersonOnSite to keep track of whether the repairperson is currently available; more on this point below.
Here is the core code, executed when a machine goes down:
MachineClass.NUp -= 1
if not G.RepairPersonOnSite:
if MachineClass.NUp < MachineClass.K:
G.RPArrive.signal()
G.RepairPersonOnSite = True
else: yield waitevent,self,G.RPArrive
yield request,self,G.RepairPerson
If the repairperson is on site already, then we go straight to the yield request to queue up for repair. If
the repairperson is not on site, and the number of working machines has not yet dropped below K, our
machine executes yield waitevent on our action G.RPArrive, which basically passivates this thread. If on
the other hand our machines failure does make the number of working machines drop below K, we execute
the signal() function, which reactivates all the machines which had been waiting.
Again, all of that could have been done via explicit passivate() and reactivate() calls, but its much more
convenient to let SimPy do that work for us, behind the scenes.
One of the member variables of SimEvent is occurred, which of course is a boolean variable stating whether
the action has occurred yet. Note that as soon as a wait for an event finishes, this variable reverts to False.
This is why we needed a separate variable above, G.RepairPersonOnSite.
4.2
In general thread terminology, we say that we post a signal when we call signal(). One of the issues to
resolve when you learn any thread system concerns what happens when a signal is posted before any waits
for it are executed. In many thread systems, that posting will be completely ignored, and subsequent waits
will thus last forever, or at least until another signal is posted. This obviously can cause bugs and makes
programming more difficult.
In SimPy its the opposite: If a signal is posted first, before any waits are started, the next wait will return
immediately. That was not an issue in this program, but its important to keep in mind in general.
4.3
You can also use yield waiteventj to wait for several actions, producing a whichever comes first operation.
To do this, instead of using the form
yield waitevent,self, action name
14
use
yield waitevent,self, tuple or list of action names
Then whenever a signal is invoked on any one of the specified actions, all waits queued will be reactivated.
4.4
This works just like yield waitevent, but when the signal is invoked, only the action at the head of the queue
will be reactivated.
4.5
1
2
3
4
5
6
7
#
#
#
#
#
#
#
Example: Carwash
8
9
# usage:
10
11
12
13
# where:
14
15
16
17
18
19
20
21
#
#
#
#
#
#
#
22
23
24
25
26
#
#
#
#
27
28
import sys,random
29
30
31
32
33
34
35
class Globals:
Rnd = random.Random(12345)
Debug = False
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class Street(Process):
CrossRate = None
ExitTime = None
NextArrival = None # time of next street arrival
CrossArrive = SimEvent()
def Run(self):
while 1:
TimeToNextArrival = Globals.Rnd.expovariate(Street.CrossRate)
Street.NextArrival = now() + TimeToNextArrival
Street.CrossArrive.signal() # tells car at front of buffer to
# check new TimeToNextArrival
yield hold,self,TimeToNextArrival
if Globals.Debug:
15
50
51
52
print
print time,now()
print street arrival
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
class Car(Process):
NextID = 0 # for debugging
PropMostlyClean = None
CurrentCars = [] # for debugging and code verification
TotalWait = 0.0 # total wait times of all cars, from arrival to
# carwash to exit onto the street
TotalBufTime = 0.0 # total time in buffer for all cars
NStuckInBay = 0 # number of cars stuck in bay when wash done, due to
# full buffer
AllDone = 0 # number of cars that have gotten onto the street
def __init__(self):
Process.__init__(self)
self.ID = Car.NextID
Car.NextID += 1
self.ArrTime = None # time this car arrived at carwash
self.WashDoneTime = None # time this car will finish its wash
self.LeaveTime = None # time this car will exit
self.StartBufTime = None # start of period in buffer
def Run(self): # simulates one call
self.State = waiting for bay
Car.CurrentCars.append(self)
self.ArrTime = now()
if Globals.Debug: ShowStatus(carwash arrival)
yield request,self,CarWash.Bay
self.State = in bay
if Globals.Rnd.uniform(0,1) < Car.PropMostlyClean: WashTime = 1.0
else: WashTime = 2.0
self.WashDoneTime = now() + WashTime
if Globals.Debug: ShowStatus(start wash)
yield hold,self,WashTime
self.State = waiting for buffer
self.WashDoneTime = None
if Globals.Debug: ShowStatus(wash done)
if CarWash.Buf.n == 0: Car.NStuckInBay += 1
yield request,self,CarWash.Buf
self.StartBufTime = now()
yield release,self,CarWash.Bay
self.State = in buffer
if Globals.Debug: ShowStatus(got into buffer)
yield request,self,CarWash.BufFront
# OK, now wait to get out onto the street; every time a new car
# arrives in cross traffic, it will signal us to check the new
# next arrival time
while True:
PossibleLeaveTime = now() + Street.ExitTime
if Street.NextArrival >= PossibleLeaveTime:
self.State = on the way out
self.LeaveTime = PossibleLeaveTime
if Globals.Debug: ShowStatus(leaving)
yield hold,self,Street.ExitTime
Car.CurrentCars.remove(self)
self.LeaveTime = None
Car.TotalWait += now() - self.ArrTime
Car.TotalBufTime += now() - self.StartBufTime
if Globals.Debug: ShowStatus(gone)
Car.AllDone += 1
yield release,self,CarWash.BufFront
yield release,self,CarWash.Buf
return
yield waitevent,self,Street.CrossArrive
114
115
116
117
class CarWash(Process):
ArrRate = None
BufSize = None
16
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
def main():
if debug in sys.argv: Globals.Debug = True
CarWash.ArrRate = float(sys.argv[1])
Car.PropMostlyClean = float(sys.argv[2])
CarWash.BufSize = int(sys.argv[3])
CarWash.Buf = Resource(CarWash.BufSize)
Street.CrossRate = float(sys.argv[4])
Street.ExitTime = float(sys.argv[5])
initialize()
CWArr = CarWash()
activate(CWArr,CWArr.Run())
StArr = Street()
activate(StArr,StArr.Run())
MaxSimtime = float(sys.argv[6])
BMC = BufMonClass()
BMC.PrSmp = PerSmp(0.1,BMC.BufMon,BMC.RecordNInBuf)
activate(BMC.PrSmp,BMC.PrSmp.Run())
simulate(until=MaxSimtime)
print number of cars getting onto the street,Car.AllDone
print mean total wait:,Car.TotalWait/Car.AllDone
MeanWaitInBuffer = Car.TotalBufTime/Car.AllDone
print mean wait in buffer:,MeanWaitInBuffer
print proportion of cars blocked from exiting bay:, \
float(Car.NStuckInBay)/Car.AllDone
print "mean number of cars in buffer, using Littles Rule:", \
MeanWaitInBuffer * CarWash.ArrRate
print mean number of cars in buffer, using alternate method:, \
BMC.BufMon.mean()
183
184
17
The default queuing discipline, i.e. priority policy, for the Resource class is First Come, First Served
(FCFS). The alternative is to assign different priorities to threads waiting for the resource, which you do by
the named argument qType. For example,
R = Resource(8,qType=PriorityQ)
creates a resource R with eight service units, the queue for which has priorities assigned. The priorities are
specified in the yield request statement. For instance,
yield request,self,R,88
requests to use the resource R, with priority 88. The priorities are user-defined.
5.1
Below is an example of a model in which we use the non-FCFS version of Resource. Here we have a
shared network channel on which both video and data are being transmitted. The two types of traffic act in
complementary manners:
We can tolerate a certain percentage of lost video packets, as small loss just causes a bit of jitter on
the screen. But we cant have any noticeable delay.
We can tolerate a certain increase in delay for data packets. We wont care about or even notice a
small increase in delay. But we cant lose packets.
Accordingly,
We discard video packets that are too old, with threshold being controlled by the design parameter
L explained in the comments in the program below.
We dont discard data packets.
For a fixed level of data traffic, we can for example use simulation to study the tradeoff arising from our
choice of the value of L. Smaller L means more lost video packets but smaller delay for data, and vice versa.
Here is the program:
1
#!/usr/bin/env python
2
3
# QoS.py:
4
5
6
7
8
9
#
#
#
#
#
18
10
11
12
13
# usage:
14
15
16
17
18
19
20
21
22
class G: # globals
Rnd = Random(12345)
Chnl = None # our one channel
VA = None # our one video arrivals process
DA = None # our one video arrivals process
23
24
25
26
27
28
29
30
class ChannelClass(Resource):
def __init__(self):
# note arguments to parent constructor:
Resource.__init__(self,capacity=1,qType=PriorityQ)
# if a packet is currently being sent, here is when transmit will end
self.TimeEndXMit = None
self.NWaitingVid = 0 # number of video packets in queue
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class VidJob(Process):
def __init__(self):
Process.__init__(self)
def Run(self):
Lost = False
# if G.Chnl.TimeEndXMit is None, then no jobs in the system
# now, so this job will start right away (handled below);
# otherwise:
if G.Chnl.TimeEndXMit != None:
# first check for loss
TimeThisPktStartXMit = G.Chnl.TimeEndXMit + G.Chnl.NWaitingVid
if TimeThisPktStartXMit - now() > VidArrivals.L:
Lost = True
VidArrivals.NLost += 1
return
G.Chnl.NWaitingVid += 1
yield request,self,G.Chnl,1 # higher priority
G.Chnl.NWaitingVid -= 1
G.Chnl.TimeEndXMit = now() + 0.999999999999
yield hold,self,0.999999999999 # to avoid coding "ties"
G.Chnl.TimeEndXMit = None
yield release,self,G.Chnl
54
55
56
57
58
59
60
61
62
63
64
65
66
class VidArrivals(Process):
L = None # threshold for discarding packet
NArrived = 0 # number of video packets arrived
NLost = 0 # number of video packets lost
def __init__(self):
Process.__init__(self)
def Run(self):
while 1:
yield hold,self,2.0
VidArrivals.NArrived += 1
V = VidJob()
activate(V,V.Run())
67
68
69
70
71
72
73
74
75
76
77
class DataJob(Process):
def __init__(self):
Process.__init__(self)
self.ArrivalTime = now()
def Run(self):
yield request,self,G.Chnl,0 # lower priority
XMitTime = G.Rnd.randint(1,6) - 0.000000000001
G.Chnl.TimeEndXMit = now() + XMitTime
yield hold,self,XMitTime
G.Chnl.TimeEndXMit = None
19
78
79
80
DataArrivals.NSent += 1
DataArrivals.TotWait += now() - self.ArrivalTime
yield release,self,G.Chnl
81
82
83
84
85
86
87
88
89
90
91
92
class DataArrivals(Process):
DArrRate = None # data arrival rate
NSent = 0 # number of video packets arrived
TotWait = 0.0 # number of video packets lost
def __init__(self):
Process.__init__(self)
def Run(self):
while 1:
yield hold,self,G.Rnd.expovariate(DataArrivals.DArrRate)
D = DataJob()
activate(D,D.Run())
93
94
95
96
97
98
# def ShowStatus():
#
print time, now()
#
print current xmit ends at, G.Chnl.TimeEndXMit
#
print there are now,len(G.Chnl.waitQ), in the wait queue
#
print G.Chnl.NWaitingVid, of those are video packets
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
def main():
initialize()
DataArrivals.DArrRate = float(sys.argv[1])
VidArrivals.L = int(sys.argv[2])
G.Chnl = ChannelClass()
G.VA = VidArrivals()
activate(G.VA,G.VA.Run())
G.DA = DataArrivals()
activate(G.DA,G.DA.Run())
MaxSimtime = float(sys.argv[3])
simulate(until=MaxSimtime)
print proportion of video packets lost:, \
float(VidArrivals.NLost)/VidArrivals.NArrived
MeanDataDelay = DataArrivals.TotWait/DataArrivals.NSent
print mean delay for data packets:,MeanDataDelay
# use Littles Rule
print mean number of data packets in system:, \
DataArrivals.DArrRate * MeanDataDelay
118
119
if __name__ == __main__:
main()
We have chosen to make a subclass of Resource for channels. In doing so, we do have to be careful when
our subclass constructor calls Resources constructor:
Resource.__init__(self,capacity=1,qType=PriorityQ)
The named argument capacity is the number of resource units, which is 1 in our case. I normally dont
name it in my Resource calls, as it is the first argument and thus doesnt need to be named, but in this case
Ive used the name for clarity. And of course Ive put in the qType argument.
Here is where I set the priorities:
yield request,self,G.Chnl,1
...
yield request,self,G.Chnl,0
# video
# data
I chose the values 1 and 0 arbitrarily. Any values would have worked, as long as the one for video was
higher, to give it a higher priority.
20
Note that I have taken transmission times to be 0.000000000001 lower than an integer, so as to avoid ties,
in which a transmission would end exactly when messages might arrive. This is a common issue when yield
hold times are integers.
5.2
This program simulates the operation of a call-in advice nurse system, such as the one in Kaiser Permanente.
The key issue here is that the number of servers (nurses) varies through time, as the policy here is to take
nurses off the shift when the number of callers is light, and to add more nurses during periods of heavy
usage.
1
#!/usr/bin/env python
2
3
# CallCtr.py:
4
5
6
7
8
9
10
11
12
13
14
#
#
#
#
#
#
#
#
#
#
15
16
# usage:
17
18
19
20
21
22
23
24
25
26
27
28
# globals
class G:
Rnd = Random(12345)
NrsPl = None # nurse pool
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class NursePool(Process):
def __init__(self,MOL,R,TO):
Process.__init__(self)
self.Rsrc = Resource(capacity=MOL,qType=PriorityQ) # the nurses
self.MOL = MOL # maximum number of nurses online
self.R = R
self.TO = TO
self.NrsCurrOnline = 0 # current number of nurses online
self.TB = None # current timebomb thread, if any
self.Mon = Monitor() # monitors numbers of nurses online
self.PrSm = PeriodicSampler.PerSmp(1.0,self.Mon,self.MonFun)
activate(self.PrSm,self.PrSm.Run())
def MonFun(self):
return self.NrsCurrOnline
def Wakeup(NrsPl,Evt): # wake nurse pool manager
reactivate(NrsPl)
# state the cause
NrsPl.WakingEvent = Evt
if G.Debug: ShowStatus(Evt)
def StartTimeBomb(self):
self.TB = TimeBomb(self.TO,self)
activate(self.TB,self.TB.Run())
21
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
def Run(self):
self.NrsCurrOnline = self.MOL
# system starts empty, so start timebomb
self.StartTimeBomb()
# this thread is a server, usually sleeping but occasionally being
# wakened to handle an event:
while True:
yield passivate,self # sleep until an event occurs:
if self.WakingEvent == arrival:
# if system had been empty, cancel timebomb
if PtClass.NPtsInSystem == 1:
self.cancel(self.TB)
self.TB = None
else: # check for need to expand pool
# how many in queue, including this new patient?
NewQL = len(self.Rsrc.waitQ) + 1
if NewQL >= self.R and self.NrsCurrOnline < self.MOL:
# bring a new nurse online
yield release,self,self.Rsrc
self.NrsCurrOnline += 1
continue # go back to sleep
if self.WakingEvent == departure:
if PtClass.NPtsInSystem == 0:
self.StartTimeBomb()
continue # go back to sleep
if self.WakingEvent == timebomb exploded:
if self.NrsCurrOnline > 1:
# must take 1 nurse offline
yield request,self,self.Rsrc,100
self.NrsCurrOnline -= 1
self.StartTimeBomb()
continue # go back to sleep
84
85
86
87
88
89
90
91
92
93
class TimeBomb(Process):
def __init__(self,TO,NrsPl):
Process.__init__(self)
self.TO = TO # timeout period
self.NrsPl = NrsPl # nurse pool
self.TimeStarted = now() # for debugging
def Run(self):
yield hold,self,self.TO
NursePool.Wakeup(G.NrsPl,timebomb exploded)
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
class ArrivalClass(Process):
22
120
121
122
123
124
125
126
127
ArvRate = None
def __init__(self):
Process.__init__(self)
def Run(self):
while 1:
yield hold,self,G.Rnd.expovariate(ArrivalClass.ArvRate)
Pt = PtClass()
activate(Pt,Pt.Run())
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
def main():
MOL = int(sys.argv[1])
R = int(sys.argv[2])
TO = float(sys.argv[3])
initialize()
G.NrsPl = NursePool(MOL,R,TO)
activate(G.NrsPl,G.NrsPl.Run())
ArrivalClass.ArvRate = float(sys.argv[4])
PtClass.SrvRate = float(sys.argv[5])
A = ArrivalClass()
activate(A,A.Run())
MaxSimTime = float(sys.argv[6])
G.Debug = int(sys.argv[7])
simulate(until=MaxSimTime)
print mean wait =,PtClass.Mon.mean()
print mean number of nurses online =,G.NrsPl.Mon.mean()
154
155
Since the number of servers varies through time, we cannot use the SimPy Resource class in a straightforward manner, as that class assumes a fixed number of servers. However, by making use of that class priorities capability, we can achieve the effect of a varying number of servers. Here we make use of an idea from
a page on the SimPy Web site, http://simpy.sourceforge.net/changingcapacity.htm.
The way this works is that we remove a server from availability by performing a yield request with a very
high priority level, a level higher than is used for any real request. In our case here, a real request is done
via the line
yield request,self,G.NrsPl.Rsrc,1
with priority 1. By contrast, in order to take one nurse off the shift, we perform
yield request,self,self.Rsrc,100
self.NrsCurrOnline -= 1
The high priority ensures that this bogus request will prevail over any real one, with the effect that the
nurse is taken offline. Note, though, that existing services are not pre-empted, i.e. a nurse is not removed
from the shift in the midst of serving someone.
Note the necessity of the line
23
self.NrsCurrOnline -= 1
The n member variable of SimPys Resource class, which records the number of available resource units,
would not tell us here how many nurses are available, because some of the resource units are held by the
bogus requests in our scheme here. Thus we need a variable of our own, NrsCurrOnline.
As you can see from the call to passivate() in NursePool.Run(), the thread NursePool.Run() is mostly
dormant, awakening only when it needs to add or delete a nurse from the pool. It is awakened for this purpose
by the patient and timebomb classes, PtClass and TimeBomb, which call this function in NursePool:
def Wakeup(NrsPl,Evt):
reactivate(NrsPl)
NrsPl.WakingEvent = Evt
It wakes up the NursePool thread, which will then decide whether it should take action to change the size
of the nurse pool, based on the argument Evt.
For example, when a new patient call arrives, generated by the ArrivalClass thread, the latter creates a
PtClass thread, which simulates that one patients progress through the system. The first thing this thread
does is
NursePool.Wakeup(G.NrsPl,arrival)
so as to give the NursePool thread a chance to check whether the pool should be expanded.
We also have a TimeBomb class, which deals with the fact that if the system is devoid of patients for a long
time, the size of the nurse pool will be reduced. After the given timeout period, this thread awakenens the
NursePool thread with the event timebomb exploded.
By the way, since activate() requires that its first argument be a class instance rather than a class, we are
forced to create an instance of NursePool, G.NrsPl, even though we only have one nurse pool. That leads to
the situation we have with the function NursePool.Wakeup() being neither a class method nor an instance
method.
Note the use of monitors, including in our PeriodicSampler class.
I have included a function ShowStatus() to help with debugging, and especially with verification of the
program. Here is some sample output:
timebomb exploded at time 0.5
6 nurse(s) online
0 patient(s) in system
timebomb started at time 0
arrival at time 0.875581049552
5 nurse(s) online
1 patient(s) in system
timebomb started at time 0.5
service starts at time 0.875581049552
5 nurse(s) online
1 patient(s) in system
no timebomb ticking
24
25