银行里的"等不了"——SimPy Bank Renege 示例全解析

🏦 先说说这个场景

想象一下:银行只开了一个窗口,顾客一个接一个地进来,但每个人的耐心都是有限的。等得太久,拍拍屁股走人------这就是 Bank Renege 要模拟的事。

"Renege"这个词本身就很传神,意思是"食言、反悔",在排队论里特指顾客中途放弃等待的行为。这是现实服务系统中再常见不过的现象,却往往被简化模型忽略掉。SimPy 用这个例子,恰好展示了如何用极少的代码把它说清楚。

整个示例涉及两个核心机制:Resource(资源管理)Condition Events(条件事件)。后者才是真正的主角。


🔧 几个数字,先心里有数

python 复制代码
RANDOM_SEED = 42            # 固定随机种子,结果可复现
NEW_CUSTOMERS = 5           # 总共来 5 位顾客
INTERVAL_CUSTOMERS = 10.0   # 平均每隔 10 个时间单位来一人
MIN_PATIENCE = 1            # 最没耐心:等 1 个单位就走
MAX_PATIENCE = 3            # 最有耐心:最多等 3 个单位

顾客到达的间隔服从指数分布 ,均值是 10;耐心值在 1 到 3 之间均匀随机;服务时间也是指数分布,均值 12。

这里有个微妙的设计:服务时间均值(12)远大于耐心上限(3)。也就是说,一旦窗口被人占着,后来的顾客几乎必然等不住。这不是 bug,是故意埋的伏笔。

参数 均值/范围 分布类型
顾客到达间隔 均值 10 指数分布
顾客耐心 1 ~ 3 均匀分布
服务时长 均值 12 指数分布

🧩 代码拆开来看

source() --- 负责"放人进来"

python 复制代码
def source(env, number, interval, counter):
    for i in range(number):
        c = customer(env, f'Customer{i:02d}', counter, time_in_bank=12.0)
        env.process(c)
        t = random.expovariate(1.0 / interval)
        yield env.timeout(t)

这个函数干的事很单纯:按照指数分布的随机间隔,一个一个地把顾客"送进门"。每位顾客被注册为一个独立的协程进程,彼此并发运行,互不干扰。

random.expovariate(1.0 / interval) 生成的是均值为 interval 的随机数------这是泊松到达过程的标准写法,模拟真实世界里"顾客不会掐着表来"的随机性。


customer() --- 整个示例的灵魂

python 复制代码
def customer(env, name, counter, time_in_bank):
    arrive = env.now
    print(f'{arrive:7.4f} {name}: Here I am')

    with counter.request() as req:
        patience = random.uniform(MIN_PATIENCE, MAX_PATIENCE)

        results = yield req | env.timeout(patience)  # ⭐ 核心一行

        wait = env.now - arrive

        if req in results:
            print(f'{env.now:7.4f} {name}: Waited {wait:6.3f}')
            tib = random.expovariate(1.0 / time_in_bank)
            yield env.timeout(tib)
            print(f'{env.now:7.4f} {name}: Finished')
        else:
            print(f'{env.now:7.4f} {name}: RENEGED after {wait:6.3f}')

⭐ 那一行代码,值得单独说

python 复制代码
results = yield req | env.timeout(patience)

这是 SimPy 条件事件的 OR 语法,读起来几乎像一句自然语言:

"等柜台空出来,或者等我的耐心耗尽------哪个先到,就听哪个的。"

results 是一个字典,记录了哪些事件率先触发 。之后用 req in results 来判断:

  • 柜台先空出来 → req 在字典里 → 顾客走上去办业务
  • 耐心先耗尽 → req 不在字典里 → 顾客转身离开,留下一句"RENEGED"

整个"等待 or 放弃"的逻辑,就压缩在这一行里。没有 if-else 的提前判断,没有轮询,干净得让人有点意外。

🔒 with 语句的隐藏功劳

with counter.request() as req 不只是语法糖。它保证了两件事:服务结束后自动释放柜台 ;顾客放弃后自动撤销排队请求。少了这个,资源会悄悄泄漏,仿真结果就乱了。


启动仿真

python 复制代码
random.seed(RANDOM_SEED)
env = simpy.Environment()
counter = simpy.Resource(env, capacity=1)   # 一个窗口,容量为 1
env.process(source(env, NEW_CUSTOMERS, INTERVAL_CUSTOMERS, counter))
env.run()

capacity=1 是关键------整个银行就这一个窗口,所有矛盾由此而生。


📊 输出结果,逐行读一遍

yaml 复制代码
Bank renege
 0.0000 Customer00: Here I am
 0.0000 Customer00: Waited  0.000    ← 窗口空着,直接上
 3.8595 Customer00: Finished

10.2006 Customer01: Here I am
10.2006 Customer01: Waited  0.000    ← 又是空窗口,运气不错

12.7265 Customer02: Here I am        ← 顾客1还没走,窗口占着呢
13.9003 Customer02: RENEGED after 1.174  ← 等了1.174秒,忍不了,走了

23.7507 Customer01: Finished         ← 顾客1服务了将近13.5秒才完事

34.9993 Customer03: Here I am
34.9993 Customer03: Waited  0.000
37.9599 Customer03: Finished

40.4798 Customer04: Here I am
40.4798 Customer04: Waited  0.000
43.1401 Customer04: Finished

Customer02 是唯一的"受害者"。它到达时,Customer01 正在接受一次长达约 13.5 秒的服务,而 Customer02 的耐心只有区区 1.174 秒------这个差距,根本没有悬念。

后面三位顾客(03、04)到达时窗口都是空的,一路畅通,完全感受不到前面发生过什么。这种时间上的错位,正是随机仿真最有趣的地方:同一个系统,不同时刻进来,体验天差地别。


💡 把核心概念摆在一起

概念 SimPy 实现 解决的问题
单窗口资源 simpy.Resource(env, capacity=1) 模拟排队竞争
申请资源 counter.request() 顾客加入队列
条件事件(OR) `yield req env.timeout(patience)`
自动资源释放 with ... as req 防止资源泄漏
泊松到达 random.expovariate(1.0 / interval) 模拟随机到达节奏

这个示例的真正价值,不在于模拟了多少顾客,而在于它用一行 yield req | env.timeout(patience),把"人在现实中如何做决策"这件事,翻译成了仿真语言。排队、等待、放弃------生活里每天都在发生的事,在这里被几十行 Python 说得明明白白。

相关推荐
星栈1 小时前
别再满项目乱丢 String:我开始给领域错误分层了
后端·代码规范
卷无止境1 小时前
SimPy 监控与数据收集:完整指南
后端
卷无止境1 小时前
Event Latency:把"等待"这件事,交给电缆来负责
后端
武子康1 小时前
Java-10 深入浅出 MyBatis 一对多与多对多查询配置详解
java·后端
一 乐1 小时前
网上订餐系统|基于springboot的网上订餐系统设计与实现(源码+数据库+文档)
java·数据库·spring boot·后端·论文·毕设·网上订餐系统
XovH1 小时前
第14篇 Docker Compose 开发环境最佳实践:热重载与调试
后端
.Cnn2 小时前
SpringBoot 文件上传与阿里云 OSS 集成
java·spring boot·后端·阿里云
XovH2 小时前
Docker从0到1再到 Kubernetes 实战:第15篇Compose 中的服务依赖、健康检查与启动顺序
后端