Hello,CSDN的各位小伙伴们,拖更了几年后,又重新开始记录这段时间准备学习的东西啦!最近博主在学习 SimPy 的相关知识,发现国内关于 SimPy 的介绍还是相对较少,且缺乏系统性,因此博主打算用1~2周的时间,边学边记,打算从 SimPy 官方给出的几个例子出发分析 SimPy 的使用场景和用法,希望能和大家互勉!那废话不多说,我们直接开始吧!今天要学习的例程是 Bank Renege
文章目录
例程背景
这个example最初的流程是这样的:有一个银行,银行里面只有一个柜台 (Counter),众多顾客以一个随机时间到达银行,柜台一次只能服务一个用户,并会消耗一定的服务时间。抵达银行的顾客如果发现柜台有人,则需要排队等候。而每一个顾客的等待时间是有限制的,如果超过一定的时间后还没有轮到自己,那么该顾客就会离开。我们的目的就是用 SimPy 对这个流程进行建模。
例程代码分析
首先自然是导入一些必要的模块,这里不再赘述
python
import simpy
import random
接下来我们定义一些仿真相关的参数:
python
RANDOM_SEED = 2024 # 随机种子
NUM_OF_CUSTOMERS = 5 # 仿真过程中顾客的总数量
INTERVAL_CUSTOMERS = 10.0 # 每个顾客的到达时间大致相差10s
MIN_PATIENCE = 1 # 顾客的最小允许等待时间
MAX_PATIENCE = 3 # 顾客的最大允许等待时间
下面是整个仿真的关键部分:我们首先来定义单个用户的行为。即在用户到达银行后,他的一些行为。用户到达银行后,先会看counter资源是否空闲 ,如果空闲,则直接开始使用柜台一段时间 ,用完柜台后释放柜台资源 。如果柜台正在被占用,那么用户就会等待 ,如果在允许的时间内等到了空闲的counter,那么就开始使用;否则用户将离开。
我们来看看这部分代码怎么写的:
python
def customer(env, name, counter, time_in_bank):
"""
定义用户的行为 (排队等待,使用柜台,离开等)
:param env: 仿真环境
:param name: 用户的标识号
:param counter: 柜台资源
:param time_in_bank: 用户使用柜台的时间
:return:
"""
arrival_time = env.now # 用户抵达银行的时间
print('Customer: ', name, ' arrive the bank at: ', arrival_time)
with counter.request() as req:
patience = random.uniform(MIN_PATIENCE, MAX_PATIENCE) # 定义该用户的允许等待时间
results = yield req | env.timeout(patience)
waiting_time = env.now - arrival_time # 该用户的等待时间
if req in results:
# 说明用户等到了counter
print('Customer: ', name, ' is ready to use the counter at: ', env.now)
tib = random.expovariate(1.0 / time_in_bank) # 用户使用柜台的时间
yield env.timeout(tib)
print('Customer: ', name, ' finished at: ', env.now)
else:
# 说明等待时间超过了用户的最大允许等待时间
print('Customer: ', name, ' has waited for: ', waiting_time, ' cannot wait any longer!')
其中,env是由simpy创建的仿真环境的名字,env.now可以获取到当前仿真的时间。另外,我们看到这句话:with counter.request() as req:
因为 counter是仿真中的一个资源 (Resource) ,resource在simpy中的定义就是在某个时刻只能被一定数量的processes使用,我们在这里定义的customer函数就是一个process。当process使用resource时,它就变成了该resource的owner,当process使用完resource时,就必须将resource释放,以便后面其他process可以继续使用该resource。
而代码中的with...as...的语句,则可以实现process对resource的请求和释放。在with的缩进里面,我们可以写一些代码,用来表示执行一些需要使用资源的操作,一旦跳出了with的缩进,那么resource就会被自动释放。
那么对于后面的 yield req
语句,它的意思就是:当程序执行到这句话时,customer这个函数就会暂停,并等待,直到resource被分配到这个用户。一旦resource被分配到了这个用户,那么代码将继续从yield req后面的语句开始继续执行。
接下来,我们再看:yield req | env.timeout(patience)
,符号 "|"在simPy中的意思是在多个事件中进行选择,会选择最先发生的那个事件。
至此,我们对单个用户的行为建模已经有所了解,下面我们再来看看如何建模用户到达bank的过程,这就包括:用户什么时候到达bank, 再根据我们上面的代码决定用户到达bank后要干啥。
python
def customers_arrival(env, num_of_customers, interval_customers, counter):
"""
定义用户抵达银行的这个随机过程
:param env: simpy的仿真环境
:param num_of_customers: 用户的总数量
:param interval_customers: 用户抵达银行的大致时间间隔
:param counter: 银行柜台 (是simpy中的resource)
:return:
"""
for i in range(num_of_customers):
c = customer(env, str(i), counter, time_in_bank=12.0)
env.process(c)
t = random.expovariate(1.0 / interval_customers)
yield env.timeout(t)
可以看到,对于每一个用户,我们首先定义了它个体到达银行后的行为,c = customer(env, str(i), counter, time_in_bank=12.0)
,将其加入到仿真环境中,然后再模拟下一个用户到达的时间。
在所有相关process都定义好后,就正式进入仿真,即我们需要定义counter资源,然后把上面定义的一些process加入到仿真环境中:
python
random.seed(RANDOM_SEED)
env = simpy.Environment() # 创建仿真环境
counter = simpy.Resource(env, capacity=1) # counter是整个仿真环境的资源,且只有一个柜台
env.process(customers_arrival(env, NUM_OF_CUSTOMERS, INTERVAL_CUSTOMERS, counter))
env.run()
仿真输出的结果为:
Customer: 0 arrive the bank at: 0
Customer: 0 is ready to use the counter at: 0
Customer: 0 finished at: 4.344581265041675
Customer: 1 arrive the bank at: 6.350494539377634
Customer: 1 is ready to use the counter at: 6.350494539377634
Customer: 1 finished at: 21.48185544267454
Customer: 2 arrive the bank at: 28.180599526311216
Customer: 2 is ready to use the counter at: 28.180599526311216
Customer: 3 arrive the bank at: 31.262456454262153
Customer: 3 has waited for: 1.8317442018885792 cannot wait any longer!
Customer: 4 arrive the bank at: 38.16001347598838
Customer: 4 has waited for: 2.9264992955047973 cannot wait any longer!
Customer: 2 finished at: 48.273541968295305
结果分析
我们可以看到,当第一个用户抵达bank时,counter空闲,因此用户0可以直接用,在4.344min用完counter并释放资源。值得注意的是:用户2使用counter的时间比较长,大概花了20min,而在此期间,用户3在第31.26min抵达bank,不巧用户3的最大允许等待时间比较短,因此,在等待了1.83min后,用户3离开。
扩展
我们希望通过对example的学习能够理解simpy其中的一些逻辑,因此,除了看懂example,我们还需要能够灵活改变源码以满足自己的想法。
如果我们想更好的模块化编程,那么用户就是一个单独的类,我们先考虑只有一个用户的情况
python
class Customer():
def __init__(self, env, name, counter, time_in_bank):
self.env = env
self.name = name
self.counter = counter
self.time_in_bank = time_in_bank
self.action = env.process(self.waiting())
def waiting(self):
arrival_time = self.env.now # 用户抵达银行的时间
print('Customer: ', self.name, ' arrive the bank at: ', arrival_time)
with counter.request() as req:
patience = random.uniform(MIN_PATIENCE, MAX_PATIENCE) # 定义该用户的允许等待时间
results = yield req | env.timeout(patience)
waiting_time = env.now - arrival_time # 该用户的等待时间
if req in results:
# 说明用户等到了counter
print('Customer: ', self.name, ' is ready to use the counter at: ', env.now)
tib = random.expovariate(1.0 / self.time_in_bank) # 用户使用柜台的时间
yield env.timeout(tib)
print('Customer: ', self.name, ' finished at: ', env.now)
else:
# 说明等待时间超过了用户的最大允许等待时间
print('Customer: ', self.name, ' has waited for: ', waiting_time, ' cannot wait any longer!')
其中,用户类里面的waiting方法就是我们原本的process,但是我们加了一句话:self.action = env.process(self.waiting())
,相当于是把waiting方法加入到仿真中了。这样,我们在定义好这个类时,process就会在对应的时间自动被触发:
python
env = simpy.Environment()
counter = simpy.Resource(env, capacity=1) # counter是整个仿真环境的资源,且只有一个柜台
C0 = Customer(env, name='C0', counter=counter, time_in_bank=12.0)
env.run()
输出如下:
Customer: C0 arrive the bank at: 0
Customer: C0 is ready to use the counter at: 0
Customer: C0 finished at: 1.7192205283488458
那么,继续,如果我们想要用很多用户,代码可以这样写:
python
def customers_arrival(env, num_of_customers, interval_customers, counter):
"""
定义用户抵达银行的这个随机过程
:param env: simpy的仿真环境
:param num_of_customers: 用户的总数量
:param interval_customers: 用户抵达银行的大致时间间隔
:param counter: 银行柜台 (是simpy中的resource)
:return:
"""
for i in range(num_of_customers):
Customer(env, name=str(i), counter=counter, time_in_bank=12.0)
t = random.expovariate(1.0 / interval_customers)
yield env.timeout(t)
因为我们在Customer类里面已经把waiting方法加入到env中了,所以此时我们只需要把Customer实例化,就能够自动执行process。因此,我们通过下面的代码启动仿真:
python
env = simpy.Environment()
counter = simpy.Resource(env, capacity=1) # counter是整个仿真环境的资源,且只有一个柜台
env.process(customers_arrival(env, NUM_OF_CUSTOMERS, INTERVAL_CUSTOMERS, counter))
env.run()
结果如下:
Customer: 0 arrive the bank at: 0
Customer: 0 is ready to use the counter at: 0
Customer: 0 finished at: 2.185043045116575
Customer: 1 arrive the bank at: 27.834642847492574
Customer: 1 is ready to use the counter at: 27.834642847492574
Customer: 1 finished at: 30.715420426177218
Customer: 2 arrive the bank at: 47.63187135568359
Customer: 2 is ready to use the counter at: 47.63187135568359
Customer: 3 arrive the bank at: 52.263879692999126
Customer: 3 has waited for: 2.0577798944440318 cannot wait any longer!
Customer: 4 arrive the bank at: 69.6644022591966
Customer: 4 has waited for: 2.1264195985524026 cannot wait any longer!
Customer: 2 finished at: 86.37400774992372
那么。进一步地,如果我们希望很多用户同时到达bank,要怎么做呢?有两种方法:
(1)不执行customers_arrival函数,直接实例化多个Customer类
python
env = simpy.Environment()
counter = simpy.Resource(env, capacity=1) # counter是整个仿真环境的资源,且只有一个柜台
customers = []
for i in range(NUM_OF_CUSTOMERS):
customers.append(Customer(env, name=str(i), counter=counter, time_in_bank=12.0))
env.run()
(2)修改customers_arrival函数,在遍历所有用户时,将yield的timeout去掉,但值得注意的是此时customers_arrival就不再是一个generator,所以不能用env.process(customers_arrival(...)),而是直接运行这个函数:
python
def customers_arrival(env, num_of_customers, interval_customers, counter):
"""
定义用户抵达银行的这个随机过程
:param env: simpy的仿真环境
:param num_of_customers: 用户的总数量
:param interval_customers: 用户抵达银行的大致时间间隔
:param counter: 银行柜台 (是simpy中的resource)
:return:
"""
for i in range(num_of_customers):
Customer(env, name=str(i), counter=counter, time_in_bank=12.0)
# t = random.expovariate(1.0 / interval_customers)
# yield env.timeout(t)
env = simpy.Environment()
counter = simpy.Resource(env, capacity=1) # counter是整个仿真环境的资源,且只有一个柜台
customers_arrival(env, NUM_OF_CUSTOMERS, INTERVAL_CUSTOMERS, counter)
# env.process(customers_arrival(env, NUM_OF_CUSTOMERS, INTERVAL_CUSTOMERS, counter))
env.run()
结果是一样的:
Customer: 0 arrive the bank at: 0
Customer: 1 arrive the bank at: 0
Customer: 2 arrive the bank at: 0
Customer: 3 arrive the bank at: 0
Customer: 4 arrive the bank at: 0
Customer: 0 is ready to use the counter at: 0
Customer: 1 has waited for: 1.1659990389377906 cannot wait any longer!
Customer: 3 has waited for: 1.5290675757676708 cannot wait any longer!
Customer: 2 has waited for: 2.3110494692657597 cannot wait any longer!
Customer: 4 has waited for: 2.856268835569244 cannot wait any longer!
Customer: 0 finished at: 18.837525165283687