A
N
I
Z
U
T
Y

OO电梯系列作业分析

在面向对象程序设计课程的第二单元的作业中,我们主要学习了线程的相关知识,设计了一部电梯。我个人觉得第一次和第二次作业难度适中,而第三次作业难度较大,上限也很高。在这三次作业中,为了保证正确性,我几乎放弃了性能分。在本单元强侧互测环节中,我一共被找出1个bug,但我也没有搭建出自己评测机,只能依靠自己在完成作业时积累的错误数据去试,最终也没能找到别人的bug。

设计分析:在本单元最后一节课,我们学到了S.O.L.I.D设计原则。

S.O.L.I.D
SRP The Single Responsibility Principle 单一责任原则
OCP The Open Closed Principle 开放封闭原则
LSP The Liskov Substitution Principle 里氏替换原则
DIP The Dependency Inversion Principle 依赖倒置原则
ISP The Interface Segregation Principle 接口分离原则
  • SRP:这三次作业中输入类仅负责向共享对象中添加请求,电梯线程仅负责移动(第二次作业)或者满足一个请求(第三次作业),调度器只负责调度(第二次作业)或分配请求(第三次作业),较好的满足了单一责任原则。
  • OCP:三次作业中前两次的可拓展性都不好,每次都重写,而第三次作业的可拓展性性较佳,将调度算法内置在电梯之中,电梯在创建时可以自选楼层,可以直接加入新的采用不同调度算法的电梯,拓展性较好。
  • LSP:第三次作业中设计了People继承PeopleRequest,在父类的基础上进行拓展,可以由子类直接替换父类,较好满足里氏替换原则。
  • DIP:三次作业中结构较为简单,没有体现此原则。
  • ISP:三次作业中没有使用接口。

第一次作业:单部多线程傻瓜调度(FAFS)电梯

完成这次作业时,我对多线程的概念还不清楚,设计主要参考了生产者消费者的设计模式。

我主要设计了三个类,分别是输入类,调度器和电梯。其中输入类和电梯分别充当了生产者和消费者的角色,而调度器是他们共享的数据,我的三次作业都基于这个基础的结构。

输入类是仿照接口文档的示例程序完成的,在线程结束前,将调度器中的结束标志设为真。

调度器中主要两个数据:一个是ArrayList,存放着乘客的请求,另一个是结束标志,标志着输入是否结束。这两个数据的读写操作我都加上了synchronized,保证线程的安全。但仔细思考后,我觉得对于结束的标志不需要加锁,原因有二:第一,一个线程对其只有写操作,另一个只有读操作,不冲突,第二这两个操作都很简单,都是原子操作。

电梯类的功能就是不断从调度器中获取请求,将乘客送到指定地点后再获取一个请求。从第一次作业开始,我就使用了wait和notifyall控制线程运行,避免了轮询,减少了CPU时间。

我认为这一次的调度还很简单,对于调度算法说不出是在哪个类,但在第二次和第三次作业之中就有了明显的区别。

可以看出,Elevator.run函数的基本复杂度比较高,在这一部分中包含了读取,移动和判断结束三个部分,设计的比较复杂,在三次作业中我的run函数中都包含了主要的功能,复杂度都很高,这是需要改进的。

第二次作业:单部多线程可捎带调度(ALS)电梯

第二次作业在整体结构上比起第一次作业没有较大的变化,还是采取了四个类的结构,主要将FAFS算法替换成了ALS算法。

本次作业中,我的电梯对象只负责运动,开关门,每当到达一层它就会向调度器发送请求,调度器给它上下乘客的名单和接下来的运动状态。相应的调度器实现就比较复杂了,不仅ALS 算法是在调度器中实现的,而且相比第一次作业,还要维护电梯中的人员名单。最初我这样设计是为第三次作业考虑。当多部电梯协同运动时,如果将电梯的运行策略固定在电梯中,就很难实现电梯之间的配合;如果将电梯的运行信息完全由调度器所掌握,更容易实现电梯的配合,但是同时也会导致调度器过于复杂,在第三次作业中,我还是放弃了性能,选择了调度算法内置在电梯中的方案。但我觉得,如果要追求很高的性能,那么应该采用这次作业的方案。

另外,在这次调度器的设计中我采用了单例模式,为将来的多部电梯做好准备。

Elevator.run的基础复杂度还是很高,我认为主要的原因是,其中有关开关门和楼层变化的逻辑比较多,但是对我来说并不复杂,因此可以接受。

Scheduler函数复杂了很多,其中我觉得getInList的确复杂,这个函数里包含了ALS算法的主要逻辑,包括主请求的选取,捎带请求的选取和添加,应该对这个函数进行进一步的分解。

这次作业我也没有追求性能分数,从一个主请求结束到移动到另一个主请求之间的过程会忽略一切可能的捎带请求,导致性能分并不理想。在互测部分,我被找出了一个bug。当电梯长时间不移动后,再移动时会将当前楼层再arrive一遍。在测试时我都是一次性将数据输入,没有考虑电梯停一段时间再运行的情况,自己测试的情况还是不够多样,这一点值得反思。

第三次作业:多部多线程智能(SS)调度电梯

在第三次作业中出现了三部电梯,三部电梯运行速度不一样,开关门时间不一样。这两部分并不麻烦,关键在于三部电梯所能达到的楼层不一样,所以有一些请求必须依赖于转乘才能够实现。这次作业我做的十分紧张,有很多不到位的地方,性能并不好。和第二次作业相比,第三次作业近乎重写。原因还是在于调度的设计。最终我没能实现电梯之间的配合,只好匆匆转向了另一种思路。

第三次作业在整体结构上依旧没有较大的变化。设计的思路也很简单,依旧采用了生产者消费者模型,由三部电梯来“抢”还没有处理的请求。如果一个请求无法直接到达,那么就先将该请求的目的楼层转变为换乘楼层。当发出该请求的乘客达到换乘楼层时,再将他的出发楼层更改为换乘楼层,目的楼层变回原来的目的楼层,再重新加入请求队列。我通过这样的方式实现了对指令的拆分,为了方便达到此操作,又设计了PersonRequest的子类Person。

在设计过程中,我最担心的还是多线程中共享的数据的问题,所以我设计时只在调度器中留下了总的请求队列和结束标志,而每个电梯自己维护自己的电梯内的乘客队列和将要处理的请求队列。这样就大大降低了多线程设计的复杂度。

在实现过程中一开始非常良好,直到最后我发现了一个严重的问题,就是电梯的停止问题。按照我原来的设计,如果调度器的结束标志为真,总请求队列,电梯自己的乘客队列和请求队列都为空时,电梯就停止运行。但是加上转乘之后就少考虑了一种情况:如果有一名转乘的乘客在途中,此时结束标志变为真,另外两个电梯就停止了,这一名转乘的乘客就无法到达目的地。这时候一个电梯停止时就需要知道是否有其他的电梯正在运行。但根据最初的设计,各个电梯之间是彼此独立,不知道互相之间的存在的,所以判断电梯的终止就撑了很困难的任务。我最终的结局方法是在调度器内维护一个变量,用来统计当前需要转乘的乘客人数。但这个设计也会引来其他很多麻烦,让代码很难看。

同时,这次作业中对CPU时间有一定的要求,因此最好不要轮询。然而我在一种情况下还是会出现轮询的情况,这也是上一段的残留问题:在其他电梯有转乘乘客时,当前电梯队列都为空时就会发生轮询的情况。最后我在这里又加了一个判断,如果自己的队列为空时直接sleep,问题解决了,但是这一部分的逻辑非常混乱并且复杂。

我认为其中最需要注意的时Scheduler.refresh,这个函数的确逻辑非常复杂,很容易出错。我在这里由于上文提及的错误,设计的不好,应该注意。另外一个好玩的情况就是movingCheck函数,这个函数是遍历电梯的请求队列,如果下一个请求在高楼层则返回2,低楼层则返回1,我个人认为这个函数是非常简单的,不知道为什么度量插件认为这个函数有这么高的复杂度。

心得体会

通过这三次作业,我对多线程有了基础的认识,尤其是生产者消费者模型,也积累了一些多线程调试的经验。但对我来说,多线程调试至今仍是一个迷。由于它的不可复现性,我目前最好的方法就是记录下那个错误的数据,然后根据自己的程序,努力“想”哪里出现了问题。对于能够重复出现的错误,我在程序中插入了一些print来定位错误,从而达到调试的作用。在测试别人的代码时,我也只有肉眼测试,效率很低。关于这方面我还有很多东西需要去学习。

希望在接下来的学习过程中,能够学到更多的东西,写出更漂亮的代码。

posted @ 2019-04-24 20:35  Quarkstar  阅读(254)  评论(1编辑  收藏  举报