面向对象程序设计第二单元作业总结

面向对象程序设计第二单元作业总结

第二单元的任务是模拟多线程实时电梯系统。在三次作业中,我们的电梯系统从最初的每座一部纵向电梯,到每座多部纵向和横向电梯,乘客的需求也从最开始的单座、单层间移动变成了任意楼座楼层之间移动。在电梯系统不断复杂化的过程中,我学习了线程的创建、运行等基本操作,熟悉了多线程的设计方法,掌握并解决线程安全问题,实现了线程之间的交互。

一、同步块和锁的设置

1、第一次作业

第一次作业的结构较为简单。我的设计思路是Input线程和Schedule调度器线程共享一个乘客队列,构成一个生产者消费者模式;同时Schedule调度器线程与每个Elevator线程共享一个乘客队列,也构成一个生产者消费者模式。第一个生产者消费者模式中,Input输入乘客作为生产者,Schedule获取乘客作为消费者。第二个则是Schedule作为生产者,根据乘客需求分配到各个电梯的乘客队列。而Elevator则作为消费者。在这样的模式下,每个队列中的乘客容器以及以及记录输入是否完成的终止信号是共享对象,需要进行同步控制。具体方法就是对addPeople(),getPeople(),isEmpty(),setEnd(),isEnd()等共享对象的读写方法加入synchronized关键词设为同步方法。

2、第二次作业

第二次作业中,除了增加乘客外还添加了增加电梯的需求。与添加乘客类似的,添加电梯也可以使用生产者消费者模式,Input线程和Addelevator线程共享一个增加电梯队列,Input为生产者,Addelevator为消费者。对电梯队列中的addElevator()getElevator(),isEmpty(),setEnd(),isEnd()等共享对象读写方法加synchronized关键词设为同步方法。

3、第三次作业

第三次作业的流水线模式不能仅通过Input()输入结束作为调度线程终止信号,相比第二次作业需要增加一个全局的计数器来统计任务完成数量。这个计数器类为单例模式,其中计数器数值为共享对象,因此其中读写计数器值的方法release,acquire均需要增加synchronized关键词。同时在调度器和电梯共享的候乘队列中增加了用于描述电梯当前运行状况的参量。这些参量在电梯运行中被修改,同时调度器也会在分配乘客时获取相关数据,属于共享对象,因此我也对获取和修改参量的方法增加了synchronized关键词以实现同步。

二、调度器设计与线程交互

第一次作业中,每座电梯数量只有一台,因此调度器线程只需要将取出的乘客请求按楼座分别添加到对应楼座电梯线程的乘客队列中就可以。第二次作业中,由于每座、每层的电梯数量可以增加,因此我设计了两层调度。第一层,调度器线程负责按照不同楼座和楼层将乘客加入对应楼座、楼层的候乘队列中。而在每个楼座和楼层中都有独立的第二层调度器线程,它能够将乘客平均分配到属于该楼座、楼层的电梯中。第三次作业在第二次作业的基础上对第二层调度器线程进行了优化,使得第二层调度器能够获取所在楼座、楼层所有电梯的能力(不同电梯的速度和运载量不同)以及当前运载情况,这使得调度器能够更加合理的分配乘客,具体的交互方式是在调度器线程和电梯线程共享的乘客队列类中加入描述电梯能力、人员情况的共享数据。这些数据由电梯线程根据运行状况(进、出人时)维护,调度器线程在每次分配乘客时获得所有该楼层、楼座电梯的相关数据。

三、UML类图及UML协作图

前两次作业的UML类图在第三次的UML类图上标注,以表现架构设计的逐步变化和扩展。
第一次的UML图如下图红框所示:
image


协作图:
image


第二次的UML图如下图绿框所示:
image


协作图:
image


与第一次作业相比,第二次作业有了以下变化:

  • 增加了二层调度器Separate类实现将请求分派给多部电梯。每个楼层、楼座都有一个Separate存储属于该楼层、楼座的电梯队列。
  • 增加了AddElevator类和InputElevatorQueue类,与Input构成生产者消费者模式满足增加电梯需求。AddElevator在获取电梯后增加到对应的Separate类。
  • 为不同的生产者消费者模式对应的容器重新命名并删除多余的内部方法,保留第一次的WaitingQueue作为SeparateElevator间的容器,构造InputPeopleQueue作为InputSchedule间的容器,构造ElevatorSelectQueue作为ScheduleSeparate间的容器。

第三次作业UML图:
image


协作图:
image


第三次作业的整体结构与第二次作业类似,部分细节处有所改变:

  • 细化二层调度器Separate所包含的电梯信息,使得同一调度器内的电梯具有相同的运行楼层、楼座以及相同的可达性。同时在SeparateSchedule的共享容器ElevatorSelectQueue中增加该类电梯信息,使得Schedule类可以知道当前所有电梯的情况,便于实现乘客任务拆分的方法getStep
  • InputPeopleQueue设置为单例模式,便于电梯将未到达终点的乘客作为输入再次添加回请求队列中,实现流水线模式。
  • 由于乘客存在多次换乘情况,Input输入结束并不能作为整体输入结束的信号。因此构造单例模式的PeopleCount类判断乘客是否都到达终点。具体实现为Input类在输入同时统计输入乘客总数sum,在输入结束后调用sumPeopleCount类中的acquire方法,而Elevator在乘客出电梯时判断当前位置与乘客的重点是否相同,相同则调用一次PeopleCount类中的release方法。当Input中sumacquire方法调用结束,说明所有请求完成,此时再发出RequestEnd信号,作为其他线程结束的信号。

四、BUG分析

第一次作业中我在强测中出现了多个RTLE错误,经过分析发现Bug出现在电梯策略。我写的电梯策略会在电梯进出乘客时强制改变主请求,而不是像ALS策略描述的在主请求确定后,直到主请求完成才切换主请求。这使得我的电梯会进行多次不必要的往复运动,效率大大降低,在强测出现超时的问题。我将电梯策略改为LOOK策略后就解决了这个问题。
第二次作业在强测和互测中均没有发现bug。
第三次作业在强测中没有bug,但在互测中出现了一个bug。经过分析,这个bug产生在新增电梯和新增请求几乎同时输入,且新增请求会立刻使用新增电梯的情况下。具体原因是在新增电梯后,电梯会初始化一个性能分,在新增乘客使用该电梯时,调度器会使用这个性能分作为除数,计算电梯当前的运载情况,所以这个性能分是一个共享数据。但我未对读写性能分的方法进行同步控制,产生了read-modify-write的安全问题,性能分在未被设置的情况下就被读取,造成除0的错误。在对相关方法加入synchronized关键词进行同步控制后就解决了这个bug。

五、Hack策略

我的hack策略分为两部分:

  1. 对电梯运行的策略和运行状态合理性的检测。具体实现就是在尽可能靠后的时间输入请求,且在同一位置输入大量可稍带的请求,以检测电梯的捎带策略以及进出人员时是否出现超载的情况
  2. 对线程安全性进行测试。具体实现是在同一时间输入大量同一楼座、楼层出发的请求,以及在输入电梯后立刻使用该电梯的请求(第六次构造数据时想到了这点,但作业要求输入电梯1s后才能输入使用请求。第七次作业没有时间限制,但自己忘了于是就被hack了)。

与第一单元构造单个复杂的数据进行hack相比,本单元的hack更注重数据的组合。数据的强度主要来自于向某个线程集中输入请求时对线程安全的挑战,以及对电梯运行策略运行状态合理性的挑战。

六、心得体会

线程安全

线程安全产生的必要条件是一个对象被多个线程共享、并且至少一个线程改变其状态。这就需要我们在编程时明确哪些对象中的数据需要共享,以及访问数据的线程有哪些。将这些问题想清楚后,在设计类时就要尽可能的让拥有共享数据的类只含有这一个共享数据,即临界区最小化,并针对这个数据设计线程安全的读写方法。同时在命名这个具有共享对象的类时,可以适当的加入访问该类的线程信息,便于我们在编程时理清思路。

层次化设计

在三次作业中,我一直沿用了:输入请求->请求分配给电梯->电梯根据请求运行的处理思路,这使得我在第一次设计时就按照这三个步骤构造了两层的生产者消费者模式。后两次作业中,根据请求分配的部分产生了较大的变化,我将请求分配给电梯细化为:请求按楼座(楼层)分配->楼层(楼座)内分配电梯,通过增加一层生产者消费者模式的方法解决了请求按楼层、楼座分配的问题,后一部分则直接沿用之前的设计。剩余两部分的架构没有发生变化,这也使得我在第一次作业中写的大部分代码可以一直沿用到第三次作业,避免了重构。这充分说明层次化的设计使得各个层具有独立性,一个层不会因为另一个层内部实现改变而发生改变。且在需求发生改变时,只需要对发生改变的层次进行重写就能够满足需求。同时,在Debug的时候,层次化的设计也有利于我们精准定位bug出现的位置。

posted @ 2022-04-29 15:40  yysrW  阅读(28)  评论(1编辑  收藏  举报