事件驅動編程---隊列應用--銀行排隊模擬--學習與思考

棧,隊列這些數據結構在理解其原理上,比較簡單,實現一個簡單的隊列也不是難事。但當僅僅學習完這些簡單的基礎之後,關於隊列真正在實際的應用,還是很抽象,生疏。對於我等初學者來說,事件驅動編程的設計和思想,一時還是難以完全接受的,下邊是我學習過程中的疑問,以及思考。

這是我的學習地址:實驗樓https://zhuanlan.zhihu.com/p/21571038

歡迎朋友們指出錯誤,一起學習,分享,交流!!!

首先,問題情景。

某個銀行從早上八點開始服務並只服務到中午十二點就停止營業。假設當天銀行只提供了 w 個服務窗口進行服務,問:

  1. 平均每分鐘有多少個顧客抵達銀行?
  2. 平均每個顧客佔用服務窗口時間是多少?

首先,我們來分析銀行的排隊邏輯。我們去銀行辦理業務,首先會去取號機取號,然後等待相應的窗口呼你的號,也就是說,在你領取你的號之後,你並不知道你排的是哪個窗口。實際上,在銀行的排隊系統中,所有的用戶(VIP除外)都是排在一個隊列上的,這和買火車票,食堂打飯的排隊方式不一樣。只有一個客戶隊列,而窗口服務完畢客戶之後,從客戶隊列中調取客戶到窗口。

到此,我們整個的排隊模型就變成了:


所以我們需要這樣的幾個基礎部件
  1. 服務窗口類(會被創建 w 個)//抽象窗口
  2. 顧客隊列類(只會被創建一個)//抽象客戶排的隊
  3. 顧客結構(包含兩個隨機屬性: 到達時間, 服務時間)//抽象辦理業務的客戶

因爲顧客的結構是連接顧客隊列以及服務窗口之間的信息,所以,我們首先可以設計我們的顧客數據結構,因爲主要的數據操作部分由服務窗口完成,所以我們用簡單的結構體來表述顧客的存儲信息。
如下(customer一直拼寫錯了,湊活着看吧。。。。)
<span style="font-size:18px;">typedef struct costomer{
	//顧客的數據結構
	//顧客是隊列的數據存儲基礎,所以操作上沒要求,用結構體就ok
	int arrive_time;//顧客的隨機到達時間
	int duration;//顧客業務的隨機耗費時間
	costomer * next;//隊列我們用鏈表實現,所以節點</span>
<span style="font-size:18px;">	 // 結構體的默認構造函數???
	costomer(int arrive_time = 0,int duration = Random::uniform(RANDOM_PARAMETER)) :arrive_time(arrive_time),
		duration(duration) ,next(nullptr){}
	//在結構體的構造函數中,實現對duration的隨機數的生成
} Costomer;</span>
關於結構體的構造函數我也是第一次見到,不過真的相見恨晚!!!(大神們見笑啦)

所以,有了顧客的數據結構之後,顧客排隊隊列便很容易實現啦!!!

下邊,便開始設計我們的窗口類,
窗口類的數據基礎主要有這兩部分】
1.存儲要處理的用戶信息
2.當前窗口的工作狀態,忙碌?空閒?
相應的在這兩個數據基礎之上,還需要一些相應的類方法

在類定義之前,我們給出窗口狀態的枚舉,這也是我們在編程中很值得學習的一個技巧吧(我直接用的0,1,自愧不如)
//窗口狀態的枚舉
enum Win_Status {
	SERVICE,//服務中0
	IDLE//空閒1
};
下邊是窗口的類定義,因爲類方法簡單,所以寫成內聯函數的形式
//工作窗口類定義
class ServiceWindows {
private:
	Costomer costomer;//存儲處理客戶的信息
	Win_Status status;//表示窗口狀態
public:
	ServiceWindows()//構造函數
	{
		status = IDLE;//初始的時候空閒
	}
	void setBusy()//窗口設置爲繁忙
	{
		status = SERVICE;
	}
	void setIdle()//窗口設置爲空閒
	{
		status = IDLE;
	}

	inline void serveCustomer(Costomer &customer) {//讀取新客戶業務
		costomer = customer;
	}

	bool IsIdle()
	{
		if (status == IDLE)
			return true;
		else
			return false;
	}
	int getArriveTime()
	{
		return costomer.arrive_time;
	}
	int getDurationTime()
	{
		return costomer.duration;
	}
	
};

到此,我們的基本部件就已經準備好了,就好像,我們買回了基本的電腦部件,但能不能真的跑起來,還得需要我們去把這些組件組裝起來。

我也是第一次聽說事件驅動編程這種說法,起初解決這個如何讓系統跑起來的問題的時候,自然而然的想到了利用while()循環,但在實際的操作中,發現挺難,很多東西不好兼顧(肯定有可以實現的大神,虛心求教!!!)然後仔細的研究了這個牛叉叉的事件驅動,貌似window系統便用到了這樣的編程思想。一想,很nice呀,一學多用呀。

那正經的,什麼事事件驅動編程呢?
度娘說:http://baike.baidu.com/view/8835457.htm

因爲官方的話,大家都可以自己百度到,那我來表達我自己的理解吧。
事件驅動編程,我的理解就是,以讀取事件爲開始,並循環的讀取時間列表中的事件,並隨之分析事件的類型,做出相應的響應,直到時間列表爲空,終止程序。

前邊說過,我們實現了程序的幾個基礎的部件,但這些都是靜態的,需要我們在他們之間搭上一些方法。
下邊是我自己對這個程序如何動起來的理解
首先,事件驅動編程,我們需要一個按照事件發生的時間先後順序排序的時間列隊,程序跑起來的過程就是程序不斷讀取這些時間並做出相應的過程。

事件驅動的一般步驟:

編輯
1、確定響應事件的元素
2、爲指定元素確定需要響應的事件類型
3、爲指定元素的指定事件編寫相應的事件處理程序
4、將事件處理程序綁定到指定元素的指定事件


我們分析下,這個程序中會有幾個事件。
兩個,1.用戶到達(到達時間) 2.用戶離開(離開時間),所以對於這兩種不同的事件,我們需要設計環環相扣的處理方法。

具體如下:
1.在銀行剛啓動的時候,我們將一條用戶到達的默認事件壓入事件隊列。
2.隨後讀取這個事件,並分析這個事件的類型(到此初始化結束)
3.如果是用戶到達事件
1.那麼用戶數目++(因爲問題有需要我們統計這個)
2.隨後,產生下一個用戶到達的隨機事件,並在此基礎上生成下一個用戶到達的事件,按時間順序放入到事件隊列中。(理解這裏存在的事件傳動)
3.然後檢查是否有空的窗口,如果有,就從等待的用戶隊列的對頭調一個用戶到這個窗口。並且隨機生成這個用戶離開的時間,在這個時間的基礎上產生這個用戶離開的時間愛,放入到事件隊列中(這很重要,用戶進入窗口,伴隨着他離開事件的生成。)
``````````````````````````分割線·······················
4.如果事件類型是離開呢?
1.計算用戶的staytime(問題的需要呀)
2.查看如果客戶的等待隊列中還有人,就將客戶調到窗口來!(進窗口了,別忘了生成他的離開事件)
3.如果等待隊列沒人,就把窗口設置爲等待狀態。


說了這麼多,是不是很迷糊呢。。。在上碼子之前總結一下難以理解的地方吧

1.靜態:首先,用戶隊列,窗口類,等等,這些都是基本的部件,是靜態的,但是是基礎。
2.動態:我們引入了事件隊列(不正經的隊列)這麼個玩意,用事件來驅動程序的運行,循環的讀取事件隊列中的事件,直到事件隊列爲空,則over。
3.傳動:傳動也是靠時間來實現的。比如 。1.處理用戶到達事件的時候,會生成下一個用戶到達的隨機時間,並且在這個時間的基礎上,形成下一個用戶的到達事件,並將之加入到事件隊列中,從而實現事件隊列的擴充(因爲這是一個模擬的程序嘛) 2.當有用戶出隊進入窗口的時候,在此之後,就會隨機生成其離開的隨機時間,由此產生這個客戶離開的隨機事件,並將之加入到事件隊列中。
4.終止:關於程序的終止,便是事件隊列的空爲終止。那從3.傳動中看,事件列表會一直得到補充呀,沒錯。所以程序有變量銀行的營業時間,在每次事件入隊的時候,都要判斷,事件的時間是否超出了營業時間,從而停止事件的輸入,實現終止。


ok,差不多啦,該上新鮮的碼子啦,讀碼字應該比讀我的文字爽吧。
大神勿嘲諷呦。

#include<iostream>
#include <cstdlib>
#include <cmath>
#include<deque>
#include<ctime>
#define RANDOM_PARAMETER 100//生成隨機數的區間0-99
using namespace std;

//大大的疑問????把函數放在類裏???
class Random {//隨機數生成類
public:
	// [0, 1) 之間的服從均勻分佈的隨機值???
	static double uniform(double max = 1) {
		return ((double)std::rand() / (RAND_MAX))*max;
	}
};


typedef struct costomer{
	//顧客的數據結構
	//顧客是隊列的數據存儲基礎,所以操作上沒要求,用結構體就ok
	int arrive_time;//顧客的隨機到達時間
	int duration;//顧客業務的隨機耗費時間
	costomer * next;
	 // 結構體的默認構造函數???
	costomer(int arrive_time = 0,int duration = Random::uniform(RANDOM_PARAMETER)) :arrive_time(arrive_time),
		duration(duration) ,next(nullptr){}
	//在結構體的構造函數中,實現對duration的隨機數的生成
} Costomer;



//窗口狀態的枚舉
enum Win_Status {
	SERVICE,//服務中0
	IDLE//空閒1
};


//工作窗口類定義
class ServiceWindows {
private:
	Costomer costomer;//存儲處理客戶的信息
	Win_Status status;//表示窗口狀態
public:
	ServiceWindows()//構造函數
	{
		status = IDLE;//初始的時候空閒
	}
	void setBusy()//窗口設置爲繁忙
	{
		status = SERVICE;
	}
	void setIdle()//窗口設置爲空閒
	{
		status = IDLE;
	}

	inline void serveCustomer(Costomer &customer) {//讀取新客戶業務
		costomer = customer;
	}

	bool IsIdle()
	{
		if (status == IDLE)
			return true;
		else
			return false;
	}
	int getArriveTime()
	{
		return costomer.arrive_time;
	}
	int getDurationTime()
	{
		return costomer.duration;
	}
	
};



//設計事件表,即,事件的數據結構
struct Event {
	int occur_time;//事件發生的時間,用於之後的事件的排序

	//描述時間的類型,-1表示到達,》=0表示離開,並且表示相應的窗口編號
	int EventType;
	Event * next;

	//所以,又是結構體的構造函數?
	Event(int time = Random::uniform(RANDOM_PARAMETER) ,int type = -1):occur_time(time),EventType(type)
		,next(nullptr) {}
};


//可插入隊列的的實現
template<class T>
class Queue {
private:
	T * front;
	T * rear;//頭指針and尾指針
public:
	Queue();//構造函數,帶有頭節點的
	~Queue();//析構函數
	void clearQueue();//清空隊列
	T* enqueue(T & join);//入隊
	T * dequeue();//出隊
	T * orderEnqueue(Event& event);//只適用於事件入隊
	int length();//獲得隊列長度
};




//系統隊列的設計
class QueueSystem {
private:
	int total_service_time;//總的服務時間
	int total_costomer;//總的服務顧客總數
	int total_stay_time;//總的等待時間
	int windows_number;//窗口數目
	int avg_stay_time;//平均時間
	int avg_costomers;//平均顧客數目

	ServiceWindows*  windows;//創建服務窗口數組的指針
	Queue<Costomer> customer_list;//客戶排隊等待的隊列
	Queue<Event>       event_list;//時間隊列????
	Event*          current_event;//事件指針

	double run();// 讓隊列系統運行一次
	
	void init();// 初始化各種參數
	
	void end();// 清空各種參數
	
	int getIdleServiceWindow();// 獲得空閒窗口索引
	
	void customerArrived();// 處理顧客到達事件
	
	void customerDeparture();// 處理顧客離開事件

public:
	// 初始化隊列系統,構造函數
	QueueSystem(int total_service_time, int window_num);

	// 銷燬,析構函數
	~QueueSystem();

	// 啓動模擬,
	void simulate(int simulate_num);

	inline double getAvgStayTime() {
		return avg_stay_time;
	}
	inline double getAvgCostomers() {
		return avg_costomers;
	}

};




int main()
{
	
	srand((unsigned)std::time(0)); // 使用當前時間作爲隨機數種子

	int total_service_time = 240;       // 按分鐘計算
	int window_num = 4;
	int simulate_num = 100000;    // 模擬次數????這是幹嘛用的???

	QueueSystem system(total_service_time, window_num);//構建這個系統,初始化

	system.simulate(simulate_num);//開啓模擬???這又是神馬意思

	cout << "The average time of customer stay in bank: "
		<< system.getAvgStayTime() << endl;
	cout << "The number of customer arrive bank per minute: "
		<< system.getAvgCostomers() << endl;

	getchar();
	return 0;
}

template<class T>
Queue<T>::Queue()
{
	front = new T;//有一個頭節點的鏈表
	if (!front)
		exit(1);//內存分配失敗,終止程序
	rear = front;
	front->next = nullptr;//頭節點
}

template<class T>
Queue<T>::~Queue()//析構函數,清空鏈表,釋放頭節點
{
	clearQueue();
	delete front;//釋放頭節點內存
}


template<class T>
void Queue<T>::clearQueue()
{
	T *temp_node;
	//清空鏈表的時候用頭節點往前邊推進,知道最後的NULL,這個方法比較巧妙
	while (front->next) {
		temp_node = front->next;
		front->next = temp_node->next;
		delete temp_node;
	}

	this->front->next = NULL;
	this->rear = this->front;
}

template<class T>
T * Queue<T>::enqueue(T & join)
{//從隊尾加入
	T * new_node= new T;
	if (!new_node)
		exit(1);
	*new_node = join;
	new_node->next = nullptr;

	rear->next = new_node;
	rear = rear->next;
	return front;//返回頭指針,
}

template<class T>
T * Queue<T>::dequeue()//注意,這裏實現的不是刪除節點,而是將節點從鏈表拆除,拿走使用
{
	if (!front->next)//空,全面的錯誤檢查
		return nullptr;

	T * temp = front->next;
	front->next = temp->next;//將首節點拆除,以便於後來帶走
	
	if (!front->next)//錯誤預警,判斷是不是拿走的是不是最後一個元素
		rear = front;

	return temp;//返回出隊的元素指針,在這裏不釋放。
}



template<class T>
int Queue<T>::length()
{
	T *temp_node;
	temp_node = this->front->next;
	int length = 0;
	while (temp_node) {
		temp_node = temp_node->next;
		++length;
	}
	return length;
}

template<class T>
T * Queue<T>::orderEnqueue(Event & event)//對於事件列表,要按照時間的順序插入
{
	Event* temp = new Event;
	if (!temp) {
		exit(-1);
	}
	*temp = event;//賦值

	// 如果這個列表裏沒有事件, 則把 temp 事件插入
	if (!front->next) {
		enqueue(*temp);
		delete temp;
		return front;
	}

	// 按時間順序插入
	Event *temp_event_list = front;

	// 如果有下一個事件,且下一個事件的發生時間小於要插入的時間的時間,則繼續將指針後移
	while ( temp_event_list->next  &&  temp_event_list->next->occur_time < event.occur_time) {
		temp_event_list = temp_event_list->next;
	}//最終得到的temp_event_list的下一個是時間大於新輸入event的,所以應該插入在temp_event_list之後

	// 將事件插入到隊列中
	temp->next = temp_event_list->next;
	temp_event_list->next = temp;

	// 返回隊列頭指針
	return front;
}
/*
我們來看入隊方法和出隊方法中兩個很關鍵的設計:

入隊時儘管引用了外部的數據,但是並沒有直接使用這個數據,反而是在內部新分配了一塊內存,再將外部數據複製了一份。
出隊時,直接將分配的節點的指針返回了出去,而不是拷貝一份再返回。
在內存管理中,本項目的代碼使用這樣一個理念:誰申請,誰釋放。

隊列這個對象,應該管理的是自身內部使用的內存,釋放在這個隊列生命週期結束後,依然沒有釋放的內存。
*/

QueueSystem::QueueSystem(int total_service_time, int window_num):
	total_service_time(total_service_time),
windows_number(window_num),
total_stay_time(0),
total_costomer(0)
{//構造函數
	windows = new ServiceWindows[windows_number];//創建 num 個工作窗口
}

QueueSystem::~QueueSystem()
{
	delete [] windows ;//釋放窗口內存
}

void QueueSystem::simulate(int simulate_num)//這個地方一直沒搞懂,模擬?
{
	double sum = 0;//累計模擬次數????

	//這個循環可以說是這個系統跑起來運行的發動機吧
	for (int i = 0; i != simulate_num; ++i) {
		// 每一遍運行,我們都要增加在這一次模擬中,顧客逗留了多久
		sum += run();
	}


	/*模擬結束,進行計算,類似覆盤*/

	// 計算平均逗留時間
	avg_stay_time = (double)sum / simulate_num;
	// 計算每分鐘平均顧客數
	avg_costomers = (double)total_costomer / (total_service_time*simulate_num);
}


// 系統開啓運行前, 初始化事件鏈表,第一個時間一定是到達事件,所以採用默認構造就ok
void QueueSystem::init() {

	Event *event = new Event;//創建一個默認的事件,到達。
	current_event = event;//並且是當前事件
}



// 系統開始運行,不斷消耗事件表,當消耗完成時結束運行
double QueueSystem::run() {

	init();//在這裏初始化????
	
	while (current_event) {
		// 判斷當前事件類型
		if (current_event->EventType == -1) {
			customerArrived();//事件類型爲-1,處理客戶到達事件
		}
		else {
			customerDeparture();//處理客戶離開事件
		}

		delete current_event;//處理完畢,釋放當前的事件
		// 從事件表中讀取新的事件
		current_event = event_list.dequeue();//出隊列,
	};
	end();//結束

	// 返回顧客的平均逗留時間
	return (double)total_stay_time / total_costomer;
}




// 系統運行結束,將所有服務窗口置空閒。並清空用戶的等待隊列和事件列表????
void QueueSystem::end() {
	// 設置所有窗口空閒
	for (int i = 0; i != windows_number; ++i) {
		windows[i].setIdle();
	}

	// 顧客隊列清空
	customer_list.clearQueue();

	// 事件列表清空
	event_list.clearQueue();
}


// 處理用戶到達事件
void QueueSystem::customerArrived() {

	total_costomer++;//用戶數目++

	// 生成下一個顧客的到達事件

	int intertime = Random::uniform(100);  // 下一個顧客到達的時間間隔,我們假設100分鐘內一定會出現一個顧客
										   // 下一個顧客的到達時間 = 當前時間的發生時間 + 下一個顧客到達的時間間隔
	int time = current_event->occur_time + intertime;
	Event temp_event(time);//結構體構造函數,參數爲到達時間,然後業務時間在構造函數中生成
	// 如果下一個顧客的到達時間小於服務的總時間,就把這個事件插入到事件列表中
	
	if (time < total_service_time) {
		event_list.orderEnqueue(temp_event);
	} // 否則不列入事件表,且不加入 cusomer_list

		// 同時將這個顧客加入到 customer_list 進行排隊
	  // 處理當前事件中到達的顧客
	Costomer *customer = new Costomer(current_event->occur_time);
	if (!customer) {
		exit(-1);
	}
	customer_list.enqueue(*customer);//將的用戶加入列表

	// 如果當前窗口有空閒窗口,那麼直接將隊首顧客送入服務窗口
	int idleIndex = getIdleServiceWindow();
	if (idleIndex >= 0) {
		customer = customer_list.dequeue();//客戶指針
		windows[idleIndex].serveCustomer(*customer);//將客戶信息傳遞給空閒的窗口處理
		windows[idleIndex].setBusy();//窗口設置爲忙碌

		// 顧客到窗口開始服務時,就需要插入這個顧客的一個離開事件到 event_list 中
		// 離開事件的發生時間 = 當前時間事件的發生時間 + 服務時間
		Event temp_event(current_event->occur_time + customer->duration, idleIndex);
		event_list.orderEnqueue(temp_event);//將離開的事件按照時間的先後插入事件鏈表
	}
	delete customer;//釋放已經傳遞到窗口的客戶信息
}


//獲取空閒窗口的序號
int QueueSystem::getIdleServiceWindow() {
	for (int i = 0; i != windows_number; ++i) {//遍歷查找
		if (windows[i].IsIdle()) {
			return i;
		}
	}
	return -1;
}


// 處理用戶離開事件
void QueueSystem::customerDeparture() {
	// 如果離開事件的發生時間比總服務時間大,我們就不需要做任何處理
	if (current_event->occur_time < total_service_time) {
		// 顧客總的逗留時間 = 當前顧客離開時間 - 顧客的到達時間
		total_stay_time += current_event->occur_time - windows[current_event->EventType].getArriveTime();

		// 如果隊列中有人等待,則立即服務等待的顧客
		//把窗口交給排隊中的新的客戶
		if (customer_list.length()) {
			Costomer *customer;
			customer = customer_list.dequeue();
			windows[current_event->EventType].serveCustomer(*customer);

			// 因爲有新的客戶進入櫃檯,所以要爲這個新的客戶編寫離開事件事件,並送到事件列表中
			Event temp_event(
				current_event->occur_time + customer->duration,
				current_event->EventType
			);
			event_list.orderEnqueue(temp_event);

			delete customer;
		}
		else {
			// 如果隊列沒有人,且當前窗口的顧客離開了,則這個窗口是空閒的
			windows[current_event->EventType].setIdle();
		}

	}
}







發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章