🏦 先说说这个场景
想象一下:银行只开了一个窗口,顾客一个接一个地进来,但每个人的耐心都是有限的。等得太久,拍拍屁股走人------这就是 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 说得明明白白。