【SimPy系列博客之官方example学习与解读】—— Example 1: Bank Renege

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
相关推荐
数据智能老司机5 小时前
精通 Python 设计模式——分布式系统模式
python·设计模式·架构
数据智能老司机6 小时前
精通 Python 设计模式——并发与异步模式
python·设计模式·编程语言
数据智能老司机6 小时前
精通 Python 设计模式——测试模式
python·设计模式·架构
数据智能老司机6 小时前
精通 Python 设计模式——性能模式
python·设计模式·架构
c8i7 小时前
drf初步梳理
python·django
每日AI新事件7 小时前
python的异步函数
python
这里有鱼汤8 小时前
miniQMT下载历史行情数据太慢怎么办?一招提速10倍!
前端·python
databook17 小时前
Manim实现脉冲闪烁特效
后端·python·动效
程序设计实验室17 小时前
2025年了,在 Django 之外,Python Web 框架还能怎么选?
python
倔强青铜三19 小时前
苦练Python第46天:文件写入与上下文管理器
人工智能·python·面试