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

来源:互联网 发布:java开发webservice 编辑:程序博客网 时间:2024/05/16 10:06

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

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

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

首先,问题情景。

某个银行从早上八点开始服务并只服务到中午十二点就停止营业。假设当天银行只提供了 w 个服务窗口进行服务,问:

  1. 平均每分钟有多少个顾客抵达银行?
  2. 平均每个顾客占用服务窗口时间是多少?

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

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


所以我们需要这样的几个基础部件
  1. 服务窗口类(会被创建 w 个)//抽象窗口
  2. 顾客队列类(只会被创建一个)//抽象客户排的队
  3. 顾客结构(包含两个随机属性: 到达时间, 服务时间)//抽象办理业务的客户

因为顾客的结构是连接顾客队列以及服务窗口之间的信息,所以,我们首先可以设计我们的顾客数据结构,因为主要的数据操作部分由服务窗口完成,所以我们用简单的结构体来表述顾客的存储信息。
如下(customer一直拼写错了,凑活着看吧。。。。)
<span style="font-size:18px;">typedef struct costomer{//顾客的数据结构//顾客是队列的数据存储基础,所以操作上没要求,用结构体就okint 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,//服务中0IDLE//空闲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;elsereturn 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-99using namespace std;//大大的疑问????把函数放在类里???class Random {//随机数生成类public:// [0, 1) 之间的服从均匀分布的随机值???static double uniform(double max = 1) {return ((double)std::rand() / (RAND_MAX))*max;}};typedef struct costomer{//顾客的数据结构//顾客是队列的数据存储基础,所以操作上没要求,用结构体就okint 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,//服务中0IDLE//空闲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;elsereturn 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);}// 系统开启运行前, 初始化事件链表,第一个时间一定是到达事件,所以采用默认构造就okvoid 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();}}}







1 0
原创粉丝点击