您的位置:首页 > 编程语言

事件驱动编程---队列应用--银行排队模拟--学习与思考

2016-09-10 14:05 627 查看
栈,队列这些数据结构在理解其原理上,比较简单,实现一个简单的队列也不是难事。但当仅仅学习完这些简单的基础之后,关于队列真正在实际的应用,还是很抽象,生疏。对于我等初学者来说,事件驱动编程的设计和思想,一时还是难以完全接受的,下边是我学习过程中的疑问,以及思考。

这是我的学习地址:实验楼https://zhuanlan.zhihu.com/p/21571038

欢迎朋友们指出错误,一起学习,分享,交流!!!

首先,问题情景。

某个银行从早上八点开始服务并只服务到中午十二点就停止营业。假设当天银行只提供了 w 个服务窗口进行服务,问:
平均每分钟有多少个顾客抵达银行?
平均每个顾客占用服务窗口时间是多少?

首先,我们来分析银行的排队逻辑。我们去银行办理业务,首先会去取号机取号,然后等待相应的窗口呼你的号,也就是说,在你领取你的号之后,你并不知道你排的是哪个窗口。实际上,在银行的排队系统中,所有的用户(VIP除外)都是排在一个队列上的,这和买火车票,食堂打饭的排队方式不一样。只有一个客户队列,而窗口服务完毕客户之后,从客户队列中调取客户到窗口。

到此,我们整个的排队模型就变成了:



所以我们需要这样的几个基础部件

服务窗口类(会被创建 w 个)//抽象窗口
顾客队列类(只会被创建一个)//抽象客户排的队
顾客结构(包含两个随机属性: 到达时间, 服务时间)//抽象办理业务的客户

因为顾客的结构是连接顾客队列以及服务窗口之间的信息,所以,我们首先可以设计我们的顾客数据结构,因为主要的数据操作部分由服务窗口完成,所以我们用简单的结构体来表述顾客的存储信息。
如下(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();
}

}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  c++ 数据结构