4. 离散事件仿真原理与排队系统仿真框架

排队系统的本质是“状态在随机时刻发生跳变”的过程:顾客到达、开始服务、服务结束、顾客离开,这些动作并非连续发生,而是在离散的时间点上改变系统状态。离散事件仿真(Discrete Event Simulation, DES)正是围绕这一特征构建的:它不按固定步长推进时间,而是让仿真时钟从一个事件时刻直接跳到下一个事件时刻,从而跳过大量“无事发生”的空闲区间。理解事件调度、仿真时钟、状态变量与统计计数器这四个核心机制,是设计通用排队仿真引擎的前提。

四个核心机制及其相互关系

仿真时钟是引擎的“当前时间”变量,记为 t_now。它只在处理事件时被更新,更新方式是直接赋值为下一个待处理事件的发生时刻,而不是 t_now += Δt。这一“时间跳跃”特性决定了 DES 的计算效率:仿真 8 小时(28800 秒)的银行网点,若平均每 30 秒发生一个事件,只需处理约千级事件,而非按秒推进的 28800 次循环。

事件调度依赖事件表(Future Event List, FEL)。事件表按发生时刻升序排列,引擎每次取出表头事件执行,执行过程中可能产生新事件并插入表中。事件通常用三元组表示:(发生时刻, 事件类型, 关联实体)。例如 (t=125.4, "到达", 顾客#7)。为保证同一时刻多个事件的处理顺序确定,可引入优先级字段,如“离开”优先于“到达”,避免资源释放与占用在同一时刻产生歧义。

状态变量描述系统在某一时刻的完整快照,排队系统中最典型的是:服务员状态(空闲/忙碌)、队列长度、队列中顾客的到达时刻列表、当前正在服务的顾客剩余服务时间等。状态变量必须足以支撑“下一事件如何改变系统”的判断,例如服务员空闲且队列非空时,应立即触发“开始服务”事件。

统计计数器在事件处理过程中累积指标:累计排队人数的时间积分、累计等待时间、服务人数、最大队列长度等。关键在于“时间加权”统计——队列长度在两次事件之间保持不变,因此面积累加应写成 area += queue_len * (t_now - t_last),再在仿真结束时除以总时长得到平均队列长度。若只在事件点采样求算术平均,会系统性偏离真实时间平均值。

排队系统 DES 引擎:事件表驱动 + 时钟跳跃 事件表 FEL(按时刻升序) t=125.4 到达 顾客#7 t=131.0 离开 顾客#5 t=142.7 到达 顾客#8 t=150.2 离开 顾客#6 表头 = 下一事件时刻 仿真时钟 t_now 125.4 ← 直接赋值 事件处理:到达 → 状态更新 队列+1;若服务员空闲则触发“开始服务” 并生成“离开”事件插入 FEL 取表头 推进 状态变量(系统快照) 服务员状态:忙碌 队列长度:3 队列到达时刻:[98.2, 110.5, 121.0] 当前服务剩余时间:6.8 决定“下一事件如何改变系统” 统计计数器(时间加权累积) 队列面积 += queue_len × (t_now − t_last) 等待时间总和 += 顾客开始服务时刻 − 到达时刻 服务人数 += 1(每次“离开”事件) 最大队列长度 = max(当前值, queue_len) 结束时:平均队长 = 队列面积 ÷ 仿真总时长 更新状态 累积 新事件插入 FEL(循环)

通用引擎的模块划分

把上述机制组织成可复用的引擎,通常划分为五个模块:事件表管理(插入、取最小、判空)、时钟推进(取表头并赋值)、事件分派器(按事件类型调用对应处理函数)、状态容器(保存服务员、队列等实体)、统计收集器(在事件前后记录时间戳并累积)。事件处理函数是唯一允许修改状态的地方,这种“单点修改”约束能显著降低调试难度。

一个最小可用的主循环伪代码如下,其中 t_last 用于时间加权统计:

初始化:t_now = 0, t_last = 0
插入首个“到达”事件到 FEL
while FEL 非空 and t_now < T_end:
    evt = FEL.pop_min()
    t_now = evt.time
    统计器.累积(t_now - t_last)   # 用旧状态填充时间区间
    t_last = t_now
    dispatch(evt)                 # 到达 / 开始服务 / 离开
统计器.收尾(T_end - t_last)

注意 累积 必须发生在状态更新之前,否则会把“事件之后”的状态错误地算进“事件之前”的时间区间。这是初学者最常见的统计偏差来源。

教学示例:单服务员队列的事件推演

设某窗口到达间隔服从均值 10 秒的指数分布,服务时间服从均值 8 秒的指数分布,仿真时长 100 秒。假设随机数生成得到前几个到达时刻为 6.2、17.5、25.1、40.3,服务时间为 5.4、9.1、7.6。推演过程如下:

时刻事件状态变化统计动作
6.2到达#1服务员空闲→立即开始服务队列面积 += 0×6.2
11.6离开#1服务员转空闲,队列为空服务人数=1,空闲面积 += 5.4
17.5到达#2立即开始服务空闲面积 += 5.9
25.1到达#3服务员忙碌,入队,队长=1队列面积 += 0×7.6
26.6离开#2出队#3开始服务,队长=0队列面积 += 1×1.5,等待=1.5

由表可见,队列面积只在“队长非零”的区间增长,等待时间在“出队开始服务”时结算。仿真结束后,平均等待时间 = 等待时间总和 ÷ 服务人数,平均队长 = 队列面积 ÷ 100。这些数值属于示例推演,用于说明机制,不代表任何真实系统的性能结论。

设计引擎时,把“事件类型—处理函数”做成注册表(如字典映射),新增“放弃排队”“优先级插队”等行为只需注册新事件与处理函数,无需改动主循环,这是通用性的关键。
常见错误:用固定步长 Δt 扫描时间并检查“是否有事件发生”,这会漏掉同一 Δt 内的多个事件,且统计精度依赖步长。DES 的正确做法是事件驱动,时钟只在事件时刻停留。

最后需要强调可复现性:所有随机抽样都应来自可设定种子的随机数流,且到达流与服务流使用独立子流,避免修改服务时间分布时扰动到达序列。引擎输出应至少包含平均等待时间、平均队长、服务员利用率与服务人数,并记录仿真时长与种子,便于结果复核与对比实验。