C++ 事件驅動型銀行排隊模擬

来源:http://www.cnblogs.com/xmwd/archive/2016/09/23/CPP_event_driven_bank_queue_simulation.html
-Advertisement-
Play Games

最近重拾之前半途而廢的C++,恰好看到了《 "C++ 實現銀行排隊服務模擬" 》,但是沒有實驗樓的會員,看不到具體的實現,正好用來作為練習。 模擬的是銀行的排隊叫號系統,所有顧客以先來後到的順序在同一個隊列中等待,當有服務視窗空閑時,則隊首的顧客接受服務,完成後則下一位顧客開始接受服務。 本實現是事 ...


最近重拾之前半途而廢的C++,恰好看到了《C++ 實現銀行排隊服務模擬》,但是沒有實驗樓的會員,看不到具體的實現,正好用來作為練習。

模擬的是銀行的排隊叫號系統,所有顧客以先來後到的順序在同一個隊列中等待,當有服務視窗空閑時,則隊首的顧客接受服務,完成後則下一位顧客開始接受服務。
本實現是事件驅動型的,處理對象是事件而不是顧客:
有2種事件:顧客到事件顧客離開事件
有2個隊列:顧客隊列和事件隊列。
程式的邏輯如下:

  1. 初始化事件隊列,填充顧客到達事件;
  2. 處理事件隊列的頭部(總是為最早發生的事件),若為“顧客到達事件”,轉向處理“顧客到達事件”,若為“顧客離開事件”,轉向處理“顧客離開事件”;
  3. 迴圈進行2,直到事件隊列為空或超出營業時間;
  4. 進行清理工作。

事件處理

主流程

永遠處理事件隊列的頭部——即最早發生的事件。

void Manager::run() {
  while (_event_queue.size() != 0) {
    _current_event = _event_queue.front();
    if (_current_event->occur_time >= _total_serve_time)//超出營業時間
      break;
    if(_customer_queue.size() == 0 && _event_queue.size() <= _service_num)//生成新的顧客到達事件
    {
      generate_arrived_event(_generate_arrived_time);
      _current_event = _event_queue.front();//update current event, deal it with order
    }

    if (_current_event->event_type == EventType::ARRIVIED)//處理顧客到達事件
      customer_arrived();
    else if (_current_event->event_type == EventType::DEPARTURE)//處理顧客離開事件
      customer_departure();
  }
}

生成顧客到達事件

有2種方法:
(1)一次性生成全部顧客到達事件
設定單位時間內到達的顧客數量,生成銀行營業時間內所有的到達事件,處理這些事件,直到事件隊列為空或是超過了銀行的營業時間。

 void Manager::generate_arrived_event(int& current_time) {//生成單位時間內的顧客到達事件併入隊,current_time是共用的
  Event* event;
  int customer_per_minute = Random::uniform(RANDOM_PER_MINUTE);//單位時間內隨機到達的顧客數量,至少為1例
  while (customer_per_minute > 0) {
    event = new Event(current_time);
    _event_queue.enqueue(event);
    --customer_per_minute;
  }
  ++current_time;
 }
 

 _generate_arrived_time = 0;
 while(_generate_arrived_time < _total_serve_time) { //_total_serve_time是銀行的總營業時間
   generate_arrived_event(_generate_arrived_time);
 }

(2)分批生成顧客到達事件
僅在需要時生成顧客到達事件,因為顧客接受服務需要一定的時間,通常來說第1種方法生成的到達事件在達到銀行營業時間後不能全部處理完成,這就造成了時間和空間上的浪費。分批生成到達事件的實現是基於這樣的事實:如果顧客隊列為空且事件隊列的長度小於等於服務視窗的數量時,說明餘下的事件將不能讓服務視窗滿載並產生等待服務的顧客,此時就需要生成新的顧客到達事件。
初始化事件隊列:

 _generate_arrived_time = 0;//依舊是以時間順序生成顧客到達事件
  while(_generate_arrived_time < INIT_ARRIVIED_EVENT_NUM) {//選擇合適的值,使最初生成的顧客到達事件略多於服務視窗數量,保證服務視窗滿載且有等待顧客即可
    generate_arrived_event(_generate_arrived_time);
  }

判斷條件並生成到達事件:

 if(_customer_queue.empty() && _event_queue.size() <= _service_num)
{
  generate_arrived_event(_generate_arrived_time);
  _current_event = &_event_queue.top();//update current event, deal it with order
}

由於顧客到達事件仍是以時間順序從0到營業時間結束來生成的,之所以能夠保證不會在處理了98分鐘時的顧客離開事件(可能是5分鐘時到達的顧客產生的)後再去處理新插入的10分鐘時的顧客到達事件,就是初始化事件隊列時選擇合適的INIT_ARRIVIED_EVENT_NUM的意義所在:時刻保證事件隊列的長度大於服務視窗的數量,新生成的顧客到達事件的時間若早於顧客離開事件的時間(以時間為順序生成顧客到達事件就保證了新生成的顧客到達事件的時間不小於事件隊列中已有的顧客到達事件,而顧客離開事件的時間是隨機的,若提前於新生成的顧客到事件處理,可能會失序)也能被正確處理。

生成顧客離開事件

顧客隊列頭部顧客接受服務並出隊,生成顧客離開事件併入隊事件隊列。顧客離開事件的發生時間 = 當前時間 + 顧客接受服務的時長。

void Manager::generate_departure_event(int service_index, int current_time) {
  _services[service_index].serve_customer(*_customer_queue.front());
  _services[service_index].set_busy();//服務視窗置為“忙”
  _services[service_index].set_service_start_time(current_time);//服務開始時間
  _customer_queue.dequeue();

  int duration = _services[service_index].get_customer_duration();
  Event* event = new Event(current_time + duration, EventType::DEPARTURE, service_index);//生成顧客離開事件
  _event_queue.enqueue(event);
}

處理顧客到達事件

處理“顧客到達事件”的邏輯:

  1. 生成1個顧客,入隊顧客隊列;
  2. 出隊事件隊列;
  3. 若有空閑的服務視窗,則生成“顧客離開事件”。

下麵是代碼:

void Manager::customer_arrived() {
  int idle_service_num = get_idle_service_index();//獲取空閑的服務視窗,返回-1說明未找到
  int current_time = _current_event->occur_time;
  Customer* customer = new Customer(current_time);//顧客到達事件發生時間即為顧客到達時間, 顧客接受服務的時長隨機
  _customer_queue.enqueue(customer);
  _event_queue.dequeue();
    
  if (idle_service_num != -1)
    generate_departure_event(idle_service_num, current_time);
}

處理顧客離開事件

處理“顧客離開事件”的邏輯:

  1. 顧客所在服務視窗置為空閑,統計顧客信息;
  2. 出隊事件隊列;
  3. 若顧客隊列不為空且有空閑的服務視窗,生成“顧客離開事件”。

下麵是代碼:

void Manager::customer_departure() {
  int current_time = _current_event->occur_time;
  int service_index = _current_event->service_index;//顧客離開的服務視窗

  _customer_stay_time += current_time -
          _services[service_index].get_customer_arrive_time();//統計顧客在銀行的滯留時間
  ++_total_served_customer_num;//接受服務的顧客數目加1
  _services[service_index].set_idle();
  _event_queue.dequeue();

  if(_customer_queue.size() > 0) {
    service_index = get_idle_service_index();//有顧客離開,必然可以獲得1個空閑服務視窗,這裡獲取最小序號的服務視窗
    generate_departure_event(service_index, current_time);
  }
}

清理工作:

  1. 尋找仍在接受服務的顧客並統計他們的信息;
  2. 釋放動態申請的記憶體。

下麵是代碼:

void Manager::end() {
  for (int i = 0; i < _service_num; i++) {
    if (!_services[i].is_idle()) {//統計正在接受服務的顧客的信息
      int service_start_time = _services[i].get_service_start_time();
      int arrive_time = _services[i].get_customer_arrive_time();
      int duration = _services[i].get_customer_duration();

      _customer_stay_time += service_start_time + duration - arrive_time;
      ++_total_served_customer_num;
    }
  }

  //釋放動態申請的記憶體
  _customer_queue.clear();
  _event_queue.clear();
  delete[] _services;
}

關於隊列的說明

程式中使用的是自定義的隊列,根據需求,可以使用STL中的優先隊列和隊列,前者用於事件隊列,後者用於顧客隊列。
優先隊列的頭部總是優先順序最高的節點,對於事件來說,就是發生的時間越早,事件優先順序越高,所以這是一個最小堆——時間發生的時間最小(最早)的位於堆頂。這涉及到對Event類型的比較,使用STL的greater<Event>(需要重載operator>)或是自定義的函數對象來比較2個Event對象:
(1)重載operator>運算符

//聲明使用greater<Event>作為比較函數的優先隊列
std::priority_queue<Event, std::vector<Event>, std::greater<Event>> _event_queue;
//event_queue.h

#ifndef BANKQUEUE_EVENT_QUEUE_H
#define BANKQUEUE_EVENT_QUEUE_H

#include "random.h"

enum class EventType : int {
  ARRIVIED,
  DEPARTURE
};

class Event {
 public:
  Event():occur_time(Random::uniform(RANDOM_PARAMETER)),
          event_type(EventType::ARRIVIED),
          service_index(-1),
          next(nullptr){}

  Event(int occur_time):occur_time(occur_time),
          event_type(EventType::ARRIVIED),
          service_index(-1),
          next(nullptr){}
  Event(int occur_time, EventType event_type, int service_index):
          occur_time(occur_time),
          event_type(event_type),
          service_index(service_index),
          next(nullptr) {}

  friend bool operator< (const Event& event1, const Event& event2);//模仿STL的實現,都是通過'<'來完成餘下的比較操作符
  friend bool operator> (const Event& event1, const Event& event2);//供`greater<Event>`使用

 public:
  int occur_time;
  int service_index;
  EventType event_type;
  Event *next;
};

inline bool operator< (const Event& event1, const Event& event2) {
  return event1.occur_time < event2.occur_time;
}

inline bool operator> (const Event& event1, const Event& event2) {
  return event2 < event1;//通過'<'實現'>'的功能
}

#endif //BANKQUEUE_EVENT_QUEUE_H

(2)比較直觀且簡單的做法是自定義用於比較的函數對象:

//聲明使用EventComp作為比較函數的優先隊列
std::priority_queue<Event, std::vector<Event>, EventComp> _event_queue;
struct EventComp
{
  bool operator()(const Event& lhs, const Event& rhs) const
  {
    return lhs.occur_time > rhs.occur_time;//occur_time是公有的,若是私有的,則需要提供介面
  }
};

可以在test.h中通過USE_SELF_DEFINE_QUEUE巨集來切換2種隊列的使用。

事件隊列的順序

事件隊列要求隊首總是發生時間最早的事件,最小堆是非常好的選擇,通過前面介紹的STL優先隊列可以輕鬆實現。自定義的隊列則使用的是蠻力法,在事件入隊時就進行排序,保證隊列是以發生時間升序的,在隊列中元素較多時(如幾百個),效率是低於使用STL優先隊列的方法的,這也是為何要分批生成顧客到達事件的原因之一:防止事件隊列的元素過多。
顧客隊列是不需要排序的,所以以模板特例化的方式實現了事件隊列的入隊方法:

template<>
void Queue<Event>::enqueue(Event* event) {

  Event *cur = _front, *prev = nullptr;

  while (cur != nullptr) {
    if (cur->occur_time < event->occur_time) {
      prev = cur;
      cur = cur->next;
    }
    else
      break;
  }

  if (prev == nullptr) {
    event->next = _front;
    _front = event;
    if (_rear == nullptr)
      _rear = event;//_rear is useless to Event queue
  }
  else {
    event->next = prev->next;
    prev->next = event;
    if (prev == _rear)
      _rear = event;
  }
  ++length;
}

完整代碼在這裡


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Atitit 自動化gui 與 發帖機 技術 1.1. Gui tech1 1.2. 自動化軟體測試1 1.3. selenium attilax1 1.4. 圖形腳本語言Sikuli1 1.5. Dom1 1.6. Jsbridge1 1.7. Browser tech1 1.1. Gui tec ...
  • Atitit.ide技術原理與實踐attilax總結 1.1. 語法著色1 1.2. 智能提示1 1.3. 類成員outline..func list1 1.4. 類型推導(type inference): 1 1.5. Remote debug1 1.6. debugging api包一個gui就 ...
  • 定義類: 訪問修飾符 class 類名{ } 訪問修飾符如:public 、priate是可選的 class是聲明類的關鍵字 按照命名規範,類名首字母大寫 例:創建“人”類,關鍵代碼: public class Person{ } ********************************** ...
  • package com.hp.io; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; ...
  • 一、 概述 1. 正則表達式的測試地址: http://tool.chinaz.com/regex/ 二、具體的常用正則表達式 1. 郵箱正則表達式: 1)\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14} 2)\w+([-+.]\w+)*@ ...
  • package com.hp.io; import java.io.FileOutputStream; import java.io.IOException; public class FileOutputStreamTest{ public static void main(String []ar ...
  • 大家好,今天我們來說說機器語言。別擔心 本人仍然是喵 不是冒牌貨。可能有人說這跨度怎麼怎麼這麼大,從iOS一下就到了機器語言。恩恩,我想說 其實瞭解一點電腦基礎的知識對於我們編程開發來說是有好處的 可以幫我們理解設計高級語言的人會從哪些方面去考慮。 好了,說了這麼多題外話,還是來聊聊正題吧。希望大 ...
  • package com.hp.io; import java.io.*; public class BufferedWriterTest{ public static void main(String args[]){ FileWriter fw=null; BufferedWriter bw=nu ...
一周排行
    -Advertisement-
    Play Games
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...