BUAA_OO_第二单元总结

OO第二单元总结

第二单元作业是完成电梯的设计。

第一次作业是完成简单的五栋楼单部可搭乘电梯的设计;第二次作业增加了横向电梯,且电梯数目可增加;第三次作业增加了换乘请求。

一、架构与调度器设计

第一次作业

架构

最初做第一次作业的时候,由于对多线程知识不够了解,所以在尝试了好多次优化失败之后,还是交了最初的傻瓜电梯,一次只送一个人。后来随着理解的加强,我在bug修复阶段又重构了第一次作业,搭建了一个比较好的架构,在这里我只谈我优化后的架构。

我的基本思路是生产者消费者模式,一共建了七个类:

  • MainClass 主线程,负责启动输入线程InputThread,启动电梯运行的线程Operate,启动每个电梯的调度器线程Schedule,并new了输入请求组成的请求等待队列waitQueue
  • Request 存储乘客请求的类,这个类与题目中给的官方personRequest类的作用基本相同,但是建立他的目的主要是为了对乘客的请求进行修改(在电梯调度时起作用,具体作用以后再讲)。
  • RequestQueue 存储请求的队列,由这个模板建立起来的对象waitQueue是输入线程与调度线程之间的共享对象,建立的elevatorQueue是每个电梯外的等待队列,建立的passengerQueue是每个电梯内的乘客队列。
  • InputThread 输入线程,读入请求并存储到waitQueue中。
  • Schedule 调度器线程,负责将waitQueue中的乘客请求分给对应的电梯,并存入对应的elevatorQueue中。
  • Elevator 电梯类,负责存储电梯的状态,包括电梯的名字,所在楼层楼座,运行方向,电梯内人数等,以及new一个电梯的等待队列elevatorQueue和电梯的乘客队列passengerQueue
  • Operate 控制电梯运行的线程,是调度电梯的线程,电梯搭乘以及优化算法在这里实现。

大概思路是,输入线程读取输入的乘客指令,并存入waitQueue中,之后通过调度器线程将指令分配给对应的电梯,每个电梯有一个候乘表elevatorQueue和一个乘客队列passengerQueue,电梯运行时,启动Operate线程,将elevatorQueue中的乘客取出放到passengerQueue中,当将指定乘客送达目的地后,将其从passengerQueue中移出,直至waitQueueelevatorQueuepassengerQueue均End时,结束。

乘客请求的轨迹为:

类图

时序图

第二次作业

架构

第二次作业的要求仅仅是新增了十个横向楼层,并且可以新增电梯请求,相比起第一次作业来讲,思路变化不大。

我在第二次作业时,代码的整体思路不变,但架构做出了较大的修改,新增了楼座的类Building和楼层的类Floor,以及楼座的调度器和电梯BuildingDispatchBuildingElevatorBuildingOperate以及楼层的调度器和电梯FloorDispatchFloorElevatorFloorOperate

将横向楼层与纵向楼座的电梯的处理完全分开: 仍从InputThread读入,若为新增电梯指令,则启动新的电梯线程;若为乘客请求,则将其放入waitQueue中。之后通过schedule调度器,将乘客请求分给对应的楼座或楼层的buildinngQueuefloorQueue。乘客进入楼座或楼层后,再通过相应的dispatch调度器被对应楼座或楼层分给相应的电梯(采用的是均匀分配的方法)的elevatorQueue,之后同第一次作业,进入Operate进行调度。当所有的请求队列都End时,结束。

乘客请求的轨迹为:

类图

时序图

因为building和floor两部分的操作基本相同,所以这里将两者合并,共用Dispatch、elevatorQueue、Operate。

第三次作业

架构

第三次作业是在第二次作业的基础上加上了换乘操作,对于之前的电梯来说,由于乘客请求在进入电梯时是一次性输入的,所以无法通过电梯运行线程完成换乘。

我第三次作业的思路是,在请求进入调度器前就对请求进行处理,加入Trans类将换乘请求拆分为若干个小请求,并给新增的请求增加标志位stage表明请求属于哪个阶段,增加了isStageFinished表明当前阶段是否完成。读入的请求进入waitQueue,之后进入Trans处理,进入的请求一共有三种拆分的情况,其运行的轨迹分别可能为"一字型"、"L型"、"U型",因此stage的取值为1、2、3,并且将isStageFinished都设为False,之后这些请求进入processQueueallQueue。其中,处于perocessQueue的是可以被调度的情求,即:该请求的Stage为1或者该请求的前置请求已完成;进入allQueue的请求是所有的处理完的请求,用作遍历寻找的作用。

之后思路类似第二次,从process请求里取出请求经调度Schedule进入相应的楼座或楼层,再进入相应的电梯处理。不过此时处理完后,要将该请求的isStageFinished标志位设为true,并且去allQueue中遍历,取出目前乘客的下一个请求,并将其加入processQueue中,以待分配处理。

乘客请求的轨迹为:

类图

时序图

二、电梯调度算法

关于电梯的调度算法,我采用的是ALS调度算法。

即首先选择主请求:若电梯内没人,则选择电梯外到达时间最早的请求为主请求,即elevatorQueue的第一个;若电梯内有人,则选择电梯内到达最早的请求为主请求,即passengerQueue的第一个。之后在完成主请求的基础上实现捎带策略。

捎带主要有两个地方的捎带,一个是去接主请求的过程中的捎带;一个是已经接到主请求,要送主请求的捎带。电梯每到达一层楼,先遍历passengerQueue看有没有要出的,若有,则开门;判断电梯内人数是否大于六人,若电梯内人数小于等于六人,则判断外面是否有人要进,若有人要进,则开门。之后要出的出,要进的进。

算法流程图:

ALS算法相比look算法,不确定性很大,最终电梯运行的时间长短与主请求的选择有很大的关系。但是由于第一次作业研究了很久的ALS算法,付出的时间成本有点大Orz,所以即使发现ALS的性能比look差,我也始终没有改,而是一直在思考如何改进ALS算法。我觉得ALS的算法是可优化的,并且自己也想出了一种优化方法。

ALS算法相比之下最大的缺点在于主请求的选择,那么可以以此作为切入点。在选择主请求时,不以到达时间最早的作为主请求,而以距离电梯最近请求的作为主请求,这样的话能节省较多的时间。举个例子,如果电梯现在在5楼,而电梯外有两个请求,到达时间早的一号请求在9楼,到达时间晚的二号请求在4楼,如果以一号为主请求,那么则需要5->9->4走9层楼,而如果以二号请求作为主请求,那么则5->4->9走6层楼,上下楼的时间就大大缩短了。

这样改进之后虽然性能还是比look算法要差一点点,但是也比没改进之前的ALS算法好了很多。事实证明look算法用的人多确实是有道理的,但是花了很长时间琢磨如何优化ALS,对我来说收获也蛮大的。

三、同步块设置与锁的选择

在本次作业中,我的锁的选择都是使用了synchronized关键字;关于同步块的设置,我将存取请求的队列的类RequestQueue类封装为线程安全的类,其中的所有对外的方法都使用了synchronized进行加锁。对于waitQueue来说 ,生产者为InputThread线程,往里加入请求;消费者为Schedule线程,从中取出请求进行调度。对于elevatorQueue来说,生产者为Schedule线程或Dispatch线程,往里加入请求;消费者为Operate线程,从中取出请求并处理。

多线程中最大的问题就是保证线程安全,避免轮询和死锁的现象出现。关于轮询,一些线程在没有必要工作的时候,要避免一直盲等而浪费CPU资源。因此要把握清楚线程进行必要的工作的时机,在while或for循环中慎用continue。关于死锁,要尽可能避免循环上锁的情况,一次只拿一把锁。

除此以外,我在本次作业中使用了线程安全的类CopyOnWriteArrayList<>来代替ArrayList,只要不出现越界问题,就基本可以保证线程安全。

四、bug分析

第一次作业的bug主要是性能不优导致的TLE,原因在于写ALS调度算法时没有充分考虑可稍带的情景,以至于电梯在去接主请求的很长一段时间内还是无法捎带的,因此浪费了时间。

第二次作业的bug是一个本地复现不了的bug,电梯下到了-1层,以至于到最后都没能解决Orz。

第三次还是性能不够优导致的TLE,可能是ALS无法解决的bug。

五、hack策略

本单元参与hack较少,主要是利用测试自己程序时出现的bug的点去攻击其他人的代码,看是否会出现同样的bug。

六、心得体会

本单元的作业一开始很难下手,因为刚接触多线程,不理解多线程究竟是怎样的机制。最开始以为多线程仅仅是输入线程和电梯线程,所以写了一次只送一个指令的傻瓜电梯。直到看到第一次实验的代码,才有点明白多线程要达到怎么样的效果。第一次实验的代码对我的帮助很大,我的第一次作业基本是按照实验的代码来写的,后面的两次作业也是沿用这个思路进行迭代的。

理清思路之后,难点就到了调度算法和维护线程安全上了。关于调度算法我执意选择了ALS,性能没有最优但也相对不错。本次作业遇到的最大的问题就是线程安全,一开始频繁的出现轮询和死锁现象,后来一点点分析代码结构,一点点摸清多线程,才慢慢解决了问题。总体来看,应该使同步块操作全部由共享对象完成,保证线程获得锁的时候,其他线程调用共享对象的方法时需要等待线程的锁的释放。

不过多线程带给我最大的困恼还是无法调试和bug复现不了的问题。第一次作业有一个强测点是WA,评测机报的错是电梯超载,但是本地跑是正常的;第二次作业的WA在电梯运行到了-1楼,但是本地跑也无法复现这个bug。我猜测代码中可能出现的问题,自己构造测试数据,但是一直未能成功复现,所以最后也没能解决(可能会成为历史遗留bug吧)。

posted @ 2022-05-03 12:00  _Misivoa  阅读(36)  评论(2编辑  收藏  举报