资讯动态

C++事件驱动型银行排队模拟

发布时间:2026/10/20 1:38:58 来源:尧图企业网站定制
前言「银行排队模拟」是一个典型的离散事件仿真discrete-event simulationDES问题顾客按某个随机规律到达窗口按某个随机规律服务顾客多时排队窗口空时直接办理。我们要回答的是平均等待多久、队伍多长、窗口多忙而不是某一刻谁在窗口。这类问题在运筹学、呼叫中心里反复出现C 适合它是因为事件队列、状态机和统计量都能用标准库直接表达。常见的错误做法是按固定时间步长循环每一分钟检查一次有人到达或离开。这样做有两个毛病。第一精度受步长限制事件发生在 10.5 分钟时按分钟推进只能记到 10 或 11。第二绝大多数时间步里什么也没发生循环空转。更糟的是用std::this_thread::sleep_for按真实时间推进模拟十小时就要真的等十小时结果还受调度影响。事件驱动event-driven的思路相反只处理「状态发生变化的时刻」时间从当前事件直接跳到下一个事件中间整段跳过。模拟时钟是普通的浮点数或整数变量不是系统时钟整个模拟通常在单线程里跑完和服务器里的多线程事件循环不是一回事。本文依次讲事件循环、状态设计、完整实现与统计解读。代码以 C17 为基准本机没有 C/C 工具链所有代码按标准语义推演未编译、未运行。一、事件驱动模拟的核心把时间跳着走离散事件仿真有三个基本组成部分模拟时钟记录当前模拟时间只在处理事件时前进。事件队列按时间排序的待处理事件集合模拟器每次取出最早的一个。状态变量描述系统当前状况的数据例如忙碌窗口数、等待队列、累计统计量。主循环非常短取最早事件、把时钟推进到该事件的时间、按事件类型修改状态、可能产生新的未来事件。循环直到事件队列为空或达到预设的终止条件。这里的关键约束是事件时间不能倒退。新产生的事件时间必须大于或等于当前时钟否则它会被插入到已经处理过的位置之前导致状态在时间轴上前后矛盾。用优先队列实现事件队列时这个约束不会被自动检查需要设计上保证。按时间推进方式仿真分两类方式时间推进优点缺点时间步进time-stepped固定步长每步检查状态实现简单适合连续变化量精度受步长限制大量空转事件驱动event-driven直接跳到下一事件精确、无空转、事件稀疏时高效需要维护事件队列与状态一致性银行排队的事件天然稀疏200 个顾客最多产生 400 个事件模拟时长却可能是几百分钟。二、银行场景的状态与事件设计模型假设如下有c个服务窗口每个窗口同一时刻只服务一位顾客。顾客按泊松过程到达即相邻到达时间间隔服从指数分布均值1/λ。每位顾客的服务时间服从指数分布均值1/μ与到达过程独立。排队规则为先到先服务FIFO队列容量无限顾客不会中途离开。初始时刻系统为空第一位顾客在 0 时刻到达。模型里只有两类事件事件发生时刻状态变化产生的新事件到达Arrival顾客进入银行有空闲窗口则立即占用否则入队下一个到达若立即服务还有一个离开离开Departure顾客办完业务释放窗口队列非空则队首上前被服务者的离开「离开」事件需要携带窗口编号以便释放正确的窗口。状态变量包括busy[i]记录第 i 个窗口是否忙碌busy_count用于统计利用率waiting存放顾客到达时刻now是当前模拟时间另有总等待时间、最长等待、队列长度积分、忙碌窗口积分等累计量。统计口径是核心之一。平均等待时间是「所有顾客等待时间之和 ÷ 顾客数」平均队长不是「每次事件时数一下队列」而是队列长度在时间上的加权平均平均队长 ∫ queue_length(t) dt / 总时长同理窗口利用率是「忙碌窗口数对时间的积分 ÷窗口数 × 总时长」。用事件时刻的瞬时值做平均会严重偏斜因为事件密集的时刻往往正是队列长的时刻。正确做法是在每次时钟推进时用「上一状态持续的时间 × 该状态的值」累加面积。这就是advance_time的职责。三、完整实现下面的程序把上述设计落成代码。随机数使用std::mt19937与std::exponential_distribution事件队列使用std::priority_queue配std::greater从而得到按时间最小在前的堆。// 适用于 C17#include cstddef#include iomanip#include iostream#include queue#include random#include stdexcept#include vectornamespace {double rate_from_mean(double mean) {if (!(mean 0.0)) { // 同时排除 0、负数与 NaNthrow std::invalid_argument(分布均值必须为正);}return 1.0 / mean;}} // namespaceenum class EventType { Arrival, Departure };struct Event {double time 0.0;EventType type EventType::Arrival;int teller -1;// priority_queue 默认是大顶堆用 greater 需要 operatorbool operator(const Event other) const {return time other.time;}};class BankSimulation {public:BankSimulation(int tellers,double mean_interarrival,double mean_service,int total_customers,unsigned int seed): tellers_(tellers),busy_(tellers 0 ? static_caststd::size_t(tellers) : 0U, false),total_customers_(total_customers),arrival_dist_(rate_from_mean(mean_interarrival)),service_dist_(rate_from_mean(mean_service)),rng_(seed) {if (tellers 0 || total_customers 0) {throw std::invalid_argument(窗口数与顾客数必须为正);}}void run() {schedule_arrival(0.0); // 第一位顾客在 0 时刻到达while (!events_.empty()) {const Event event events_.top();events_.pop();advance_time(event.time);if (event.type EventType::Arrival) {handle_arrival(event.time);} else {handle_departure(event.time, event.teller);}}finish_time_ now_;}void print_report() const {const double avg_wait served_ 0 ? total_wait_ / static_castdouble(served_) : 0.0;const double avg_queue finish_time_ 0.0 ? queue_area_ / finish_time_ : 0.0;const double utilization (finish_time_ 0.0 tellers_ 0)? busy_area_ / (finish_time_ * static_castdouble(tellers_)): 0.0;std::cout std::fixed std::setprecision(3) 服务完成: served_ 人\n 平均等待: avg_wait 分钟\n 最长等待: max_wait_ 分钟\n 平均队长: avg_queue 人\n 窗口利用率: utilization \n;}private:void schedule_arrival(double time) {events_.push(Event{time, EventType::Arrival, -1});}void schedule_departure(double time, int teller) {events_.push(Event{time, EventType::Departure, teller});}void advance_time(double time) {const double delta time - now_;if (delta 0.0) {queue_area_ delta * static_castdouble(waiting_.size());busy_area_ delta * static_castdouble(busy_count_);}now_ time;}void handle_arrival(double time) {arrived_;const int teller find_free_teller();if (teller 0) {occupy(teller);served_;record_wait(0.0);schedule_departure(time next_service(), teller);} else {waiting_.push(time); // 只存到达时刻等待时间在离开时算}if (arrived_ total_customers_) {schedule_arrival(time next_interarrival());}}void handle_departure(double time, int teller) {if (teller 0 || teller tellers_ || !busy_[teller]) {throw std::logic_error(离开事件与窗口状态不一致);}release(teller);if (!waiting_.empty()) {const double arrival_time waiting_.front();waiting_.pop();record_wait(time - arrival_time);served_;occupy(teller);schedule_departure(time next_service(), teller);}}int find_free_teller() const {for (int i 0; i tellers_; i) {if (!busy_[i]) {return i;}}return -1;}void occupy(int teller) {busy_[teller] true;busy_count_;}void release(int teller) {busy_[teller] false;--busy_count_;}void record_wait(double wait) {total_wait_ wait;if (wait max_wait_) {max_wait_ wait;}}double next_interarrival() { return arrival_dist_(rng_); }double next_service() { return service_dist_(rng_); }int tellers_;std::vectorbool busy_;int busy_count_ 0;int total_customers_;int arrived_ 0;int served_ 0;double now_ 0.0;double finish_time_ 0.0;double total_wait_ 0.0;double max_wait_ 0.0;double queue_area_ 0.0;double busy_area_ 0.0;std::queuedouble waiting_;std::exponential_distributiondouble arrival_dist_;std::exponential_distributiondouble service_dist_;std::mt19937 rng_;std::priority_queueEvent, std::vectorEvent, std::greaterEvent events_;};int main() {try {// 3 个窗口平均每 2 分钟到一位平均服务 5 分钟BankSimulation sim(3, 2.0, 5.0, 200, 2026);sim.run();sim.print_report();} catch (const std::exception ex) {std::cerr 模拟失败: ex.what() \n;return 1;}return 0;}rate_from_mean在初始化列表里被调用早于构造函数体。若把参数检查放在函数体里1.0 / mean_service可能已经以 0 为除数执行过了因此检查必须前移!(mean 0.0)的写法也能拒绝NaN。Event的operator只比较时间。同一时刻的到达与离开谁先处理标准不保证若需要确定性可在比较里加入类型或序号作为次序键。advance_time在修改状态之前结算上一段状态持续的面积。事件处理完后再产生新事件新事件时间不小于当前时间因此不会出现负的时间增量。now_ time放在if外面即使时间相同也保持一致。handle_departure先释放窗口再从队首拉人。被拉起的顾客继续使用刚刚空出的窗口这是 FIFO 加同质窗口的自然结果。若窗口能力不同就需要在这里做选择模型会复杂不少。结束条件是事件队列为空。因为每位到达的顾客最终都会产生一个离开事件队列排空时所有顾客都已服务完毕finish_time_就是最后一位顾客离开的时刻。四、结果解读、校验与扩展程序的输出有五项完成人数、平均等待、最长等待、平均队长、窗口利用率。解读时要注意口径平均等待的分母是所有到达的顾客包括等待时间为 0 的人只统计排过队的人会得到偏大的数字两种口径都合理但必须说明。平均队长与利用率是整个模拟区间的时间加权平均包含开头为空和结尾清空的阶段不是严格的稳态指标估计稳态应丢弃预热期数据或把模拟跑得足够长。利用率的分母是「窗口数 × 总时长」否则可能得到大于 1 的值。一个有用的 sanity check 是利特尔法则Littles law在稳态下系统中的平均顾客数L等于到达率λ乘以平均逗留时间W。本模型里平均逗留时间等于平均等待加平均服务。把输出代入若数量级明显不符通常说明统计口径或事件处理有 bug。注意利特尔法则要求稳态与合适的统计区间短时间模拟不能拿它当精确等式。另一个校验方向是和 M/M/c 排队模型的理论公式对照。M/M/c 假设泊松到达、指数服务、c 个同质窗口、无限队列与本文模型一致其平均等待有闭式解理论值与模拟值不会完全相等差异应随顾客数增加而收窄因此要用多个种子重复观察均值与波动而不是拿单次运行下结论。可扩展的方向顾客类型普通号与优先号分两个队列离开时优先取优先队列。放弃排队reneging等待超过容忍时间就离开需要新增「放弃」事件并处理「顾客已不在队列」的情况。时变到达率早高峰到达率更高需按非齐次泊松过程抽样。窗口异构不同窗口服务不同业务入队时要按业务分流。无论怎么扩展事件循环、时间推进、面积统计这三块骨架不变。常见坑点❌ 用sleep_for或系统时钟推进模拟时间。✅ 模拟时钟是普通变量事件驱动直接跳到下一事件时刻。❌ 用std::priority_queue默认比较器结果每次取到最晚的事件。✅ 默认是大顶堆要最早事件在前使用std::greaterEvent并定义operator或自定义比较器。❌ 在事件处理中安排一个早于当前时间的事件。✅ 所有新事件时间必须不小于now必要时加断言或在调度函数里检查。❌ 平均队长用「每次事件时的队列长度取平均」。✅ 用队列长度对时间的积分除以总时长事件时刻的采样是有偏的。❌ 窗口利用率用忙碌面积除以总时长忘了除以窗口数。✅ 分母是「窗口数 × 总时长」结果才落在 0 到 1 之间。❌ 把指数分布的均值直接传给std::exponential_distribution。✅ 它的构造参数是率λ均值的倒数均值 5 分钟对应 λ 0.2。总结组成本实现的选择作用模拟时钟double now_随事件跳进与真实时间解耦事件队列priority_queuegreater每次取最早事件事件类型到达、离开驱动状态变化队列状态std::queuedouble存到达时刻FIFOO(1) 入出随机源mt19937 指数分布可复现的到达与服务时间统计等待时间累加 面积积分平均等待、队长、利用率事件驱动模拟的难点不在语法而在「状态在时间轴上的一致性」每个事件只改它该改的状态派生统计通过时间积分得到时钟只前进不后退。守住这三条银行排队只是一个几十行的状态机换成呼叫中心或网络缓冲区骨架可以原样复用。模拟结果终究是对模型的推断模型假设是否成立比代码跑得多快更值得先想清楚。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑