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;
}

完整代碼在這里


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM