先说这个例子在演示什么
这个电影院的例子换了一个角度,重点展示 SimPy 里另外两个非常实用的机制:条件事件(Condition Events)和共享事件(Shared Events)。
场景很生活化:一家电影院,一个售票窗口,同时上映三部电影,每部只卖当场的票。观众陆续到来,随机选一部电影,随机想买 1 到 6 张票。关键在于------一旦某部电影票快卖完了,还在排队等这部电影的观众会集体离队(renege)。
"集体离队"这件事,用普通的轮询逻辑来实现会很麻烦。SimPy 的做法是:把"售罄"定义成一个事件,让所有等待这部电影的观众同时监听它。事件一触发,所有人同时收到信号,各自决定走还是留。这就是共享事件的精髓。
这个例子新增了哪些核心概念
在机器车间的基础上,这个例子引入了三个新东西:
| 概念 | 对应用法 | 作用 |
|---|---|---|
| 普通资源 | simpy.Resource |
有容量限制的队列资源,先来先得 |
| 条件事件 | `event_a | event_b` |
| 共享事件 | simpy.Event + .succeed() |
一个事件被触发,所有监听它的进程同时收到通知 |
| 事件结果检查 | if my_turn not in result |
判断是哪个事件触发了条件,决定后续行为 |
其中条件事件 | 是这个例子的灵魂。它让一个进程可以同时等待两件事,谁先发生就响应谁------这在现实仿真里极其常见。
代码逐行拆解
参数与导入
python
from __future__ import annotations # 支持类型注解的前向引用(Python 3.10 以下需要)
import random
from typing import Dict, List, NamedTuple, Optional # 类型注解工具
import simpy
RANDOM_SEED = 42 # 随机种子,保证结果可复现
TICKETS = 50 # 每部电影的初始票数
SELLOUT_THRESHOLD = 2 # 剩余票数低于此值时触发"售罄"事件
SIM_TIME = 120 # 仿真时长(分钟)
SELLOUT_THRESHOLD = 2 的设计很有意思:不是等票卖完才宣布售罄,而是剩余不足 2 张时就触发。这模拟了现实中"最后几张票无法满足多人购买需求"的情况。
Theater 数据结构:用 NamedTuple 组织仿真状态
python
class Theater(NamedTuple):
counter: simpy.Resource # 售票窗口,容量为 1(同时只服务一位顾客)
movies: List[str] # 电影名称列表
available: Dict[str, int] # 每部电影的剩余票数,键为电影名
sold_out: Dict[str, simpy.Event] # 每部电影对应一个"售罄"事件
when_sold_out: Dict[str, Optional[float]] # 记录每部电影的售罄时刻
num_renegers: Dict[str, int] # 记录每部电影因售罄而离队的人数
NamedTuple 在这里只是一个轻量级的数据容器,把电影院相关的所有状态打包在一起,方便在进程间传递。
sold_out 字典是理解这个例子的关键------每部电影对应一个独立的 SimPy 事件对象 。这个事件初始未触发,一旦调用 .succeed(),所有正在 yield 等待它的进程都会同时被唤醒。
主程序:搭建电影院
python
print('Movie renege')
random.seed(RANDOM_SEED) # 固定随机种子
env = simpy.Environment() # 创建仿真环境
movies = ['Python Unchained', 'Kill Process', 'Pulp Implementation']
theater = Theater(
counter=simpy.Resource(env, capacity=1),
# ★ 普通资源,容量为 1,同一时刻只有一位观众在窗口买票
# 其余人排队等待,按到达顺序服务
movies=movies,
available=dict.fromkeys(movies, TICKETS),
# 用 dict.fromkeys 快速创建字典,每部电影初始票数均为 50
# 结果:{'Python Unchained': 50, 'Kill Process': 50, 'Pulp Implementation': 50}
sold_out={movie: env.event() for movie in movies},
# ★ 为每部电影创建一个独立的 SimPy 事件对象
# env.event() 创建一个"未触发"的事件,等待后续调用 .succeed() 激活
when_sold_out=dict.fromkeys(movies),
# 初始值均为 None,售罄时记录仿真时刻
num_renegers=dict.fromkeys(movies, 0),
# 初始值均为 0,统计因售罄而离队的人数
)
env.process(customer_arrivals(env, theater)) # 启动观众到来进程
env.run(until=SIM_TIME) # 运行仿真 120 分钟
customer_arrivals:源源不断地制造观众
python
def customer_arrivals(env, theater):
"""持续生成新观众,直到仿真结束"""
while True:
# 观众到达间隔服从指数分布,均值为 0.5 分钟
# expovariate(1/0.5) = expovariate(2),平均每 0.5 分钟来一位
yield env.timeout(random.expovariate(1 / 0.5))
movie = random.choice(theater.movies) # 随机选一部电影
num_tickets = random.randint(1, 6) # 随机想买 1~6 张票
if theater.available[movie]:
# 只有该电影还有余票,才生成这位观众的进程
# 若已售罄(available=0),直接忽略,不进入队列
env.process(moviegoer(env, movie, num_tickets, theater))
这里有个小细节:if theater.available[movie] 是在观众到达时 做的预检查。但这个检查并不完全可靠------观众到达时还有票,但排队等待的过程中可能售罄。真正的售罄处理逻辑在 moviegoer 进程里。
moviegoer:整个例子的核心进程
这个函数集中展示了条件事件和共享事件的用法,值得一行一行细看。
python
def moviegoer(env, movie, num_tickets, theater):
"""
一位观众尝试购买某部电影的票。
她面临两种可能:轮到自己买票,或者电影在等待中售罄。
"""
with theater.counter.request() as my_turn:
# ★ 向售票窗口申请服务,返回一个"请求事件"
# my_turn 是一个事件对象:当轮到这位观众时,它会被触发
# with 语句确保离开时自动释放窗口(无论是买到票还是中途离开)
# ★★ 核心:条件事件,同时等待两件事
# my_turn ------ 轮到我了(获得窗口服务权)
# theater.sold_out[movie] ------ 这部电影售罄了
# | 运算符创建"或"条件:任意一个事件触发,yield 就返回
result = yield my_turn | theater.sold_out[movie]
# result 是一个字典,包含所有已触发的事件
# 通过检查 my_turn 是否在 result 中,判断是哪个事件触发了 yield
if my_turn not in result:
# 售罄事件先触发了,我还没轮到就没票了
theater.num_renegers[movie] += 1 # 离队人数 +1
return # 直接离开,with 块自动释放请求
# 能走到这里,说明轮到我了(my_turn 在 result 中)
# 检查剩余票数是否满足需求
if theater.available[movie] < num_tickets:
# 票不够了,和售票员争论一番后离开
yield env.timeout(0.5) # 争论耗时 0.5 分钟
return # 离开,不计入 renegers(这是正常购票失败)
# 购票成功
theater.available[movie] -= num_tickets # 扣减库存
if theater.available[movie] < SELLOUT_THRESHOLD:
# ★ 剩余票数低于阈值,触发"售罄"共享事件
# .succeed() 会唤醒所有正在等待这个事件的进程
# 所有还在排队等这部电影的观众,都会在下一个调度周期收到通知
theater.sold_out[movie].succeed()
# 记录售罄时刻
theater.when_sold_out[movie] = env.now
# 将剩余票数置 0(不足阈值的零散票不再出售)
theater.available[movie] = 0
# 正常购票流程耗时 1 分钟
yield env.timeout(1)
条件事件的工作原理,画一张图
这是整个例子最值得反复理解的部分。
scss
观众 A(等 Python Unchained)
│
│── yield my_turn | sold_out['Python Unchained']
│ │ │
│ │ │
│ 等待窗口轮到我 等待售罄信号
│ │ │
│ └──── 谁先触发 ───────┘
│ │
│ ┌────────┴────────┐
│ │ │
│ 轮到我了 电影售罄了
│ 去买票 离队走人
│ (my_turn in result) (my_turn not in result)
共享事件的广播效果:
arduino
sold_out['Python Unchained'].succeed() 被调用
│
├──▶ 观众 A 的 yield 返回(result 中只有 sold_out 事件)
├──▶ 观众 B 的 yield 返回
├──▶ 观众 C 的 yield 返回
└──▶ 观众 D 的 yield 返回
(所有等待这个事件的进程同时被唤醒)
一次 .succeed(),所有监听者同时响应。这就是"共享事件"名字的由来------一个事件,被多个进程共享监听。
结果输出与分析
python
for movie in movies:
if theater.sold_out[movie]:
# ★ 直接对事件对象做布尔判断
# SimPy 的 Event 对象在触发后(.succeed() 被调用后)布尔值为 True
# 未触发时为 False,这里用来判断该电影是否售罄过
sellout_time = theater.when_sold_out[movie]
num_renegers = theater.num_renegers[movie]
print(
f'Movie "{movie}" sold out {sellout_time:.1f} minutes '
f'after ticket counter opening.'
)
print(f' Number of people leaving queue when film sold out: {num_renegers}')
输出结果:
csharp
Movie "Python Unchained" sold out 38.0 minutes after ticket counter opening.
Number of people leaving queue when film sold out: 16
Movie "Kill Process" sold out 43.0 minutes after ticket counter opening.
Number of people leaving queue when film sold out: 5
Movie "Pulp Implementation" sold out 28.0 minutes after ticket counter opening.
Number of people leaving queue when film sold out: 5
"Pulp Implementation"最快售罄(28 分钟),却只有 5 人离队------说明它售罄时排队的人不多。"Python Unchained"售罄时有 16 人离队,说明它更受欢迎,积累了更长的队伍。
两个例子对比,SimPy 的机制全景
把机器车间和电影院放在一起看,SimPy 的核心机制基本就覆盖完了:
| 机制 | 机器车间 | 电影院 |
|---|---|---|
| 进程 | Machine.working / break_machine |
moviegoer / customer_arrivals |
| 超时 | env.timeout(done_in) |
env.timeout(1) |
| 资源请求 | repairman.request(priority=1) |
theater.counter.request() |
| 抢占资源 | PreemptiveResource |
--- |
| 中断 | process.interrupt() |
--- |
| 条件事件 | --- | `my_turn |
| 共享事件 | --- | sold_out[movie].succeed() |
读完这个例子,真正理解了什么
SimPy 的事件系统比很多人想象的更灵活。env.event() 创建的不只是一个"等待点",它是一个可以被任意进程触发、被任意数量的进程监听的信号对象。
| 运算符把两个事件组合成条件,让进程可以同时等待多件事,谁先发生响应谁------这种模式在排队系统、超时重试、竞争资源等场景里极其常见。
更深一层的设计哲学是:SimPy 把"等待某件事发生"这个动作,从业务逻辑里抽象出来,变成一个可以组合、可以传递、可以共享的一等公民。这让仿真代码的表达能力远超普通的状态机或轮询写法。