P1 设计结构
三次作业的架构都没有较大的改动,基本上都是靠调度器接受输入的请求并放入队列,然后调度器根据不同的电梯的当前状态来把请求分配至不同电梯的请求队列中,最后电梯再根据自己的请求队列去运行。因此接下来都以第三次的作业为例来进行讨论。
一、diagram
- MainClass类
负责创建并启动调度器线程和所有的电梯线程,然后不断地读取输入并送至调度器。 - ControllerAll类
能够读取所有电梯的信息以及所有的输入请求,然后根据电梯的状态来对请求进行分配。其成员变量requests
为共享资源数据。 - Elevator类
负责保存某个电梯的状态信息。 - ElevatorThread
负责保存某个电梯的主请求与副请求队列,并实现了电梯的运行算法。
二、UML框架图
接下来由框架图大致演示一下线程之间的交互
三、设计策略
资源分类
- 共享资源
ArrayList<PersonRequest> requests
负责保存全局的乘客请求,然后由调度器去分配这些请求给相应的电梯。由于考虑新请求的产生可能会使得请求分配最优解的变化,因此要使用再分配机制,即给每个电梯的副请求(未进入电梯的乘客请求)分配时,并不会将这些请求从requests总请求中剔除,当且仅当请求从电梯的副请求转移至主请求时(乘客进入电梯),才将requests中对应的请求剔除。ArrayList<Elevator> elevators
负责为电梯运行部分和任务调度部分提供相应的电梯状态信息。
任务调度部分主要对电梯状态进行扫描读取,以便衡量如何分配请求。
电梯运行部分主要是对电梯状态去进行改变。
- 非共享资源
ArrayList<InElevatorPersonRequest> mainRequest
保存在电梯运行线程类中,作为电梯的主请求(即目前在电梯内的乘客请求)。通过副请求向主请求的转移(乘客上电梯)增加请求,通过乘客下电梯而删减请求。ArrayList<PersonRequest> secondRequest
保存在电梯运行线程类中,作为电梯的副请求(想要上该辆电梯的乘客请求)。通过调度器的任务分配而改变请求,以及向主请求中转移而删减请求。
- 不变资源
主要定义在Define类里,由作业要求定义而来,如楼层数、电梯运行速度、电梯最大载客量等。
多线程协同和同步控制
-
加锁原则
由于两个共享资源都有可能被进行读写操作,因此在任何要扫描共享资源或者改写共享资源的类的地方均上锁。对于仅读取或改写一个共享资源的方法添加相应类型的锁;对于要读取或改写两个共享资源的方法,采用先对requests加锁,后对elevator加锁的方式,以避免死锁的产生。 -
wait和notify控制
-
用户输入
若有新的乘客请求,假如requests队列,并notify()调度器线程。 -
调度器分配
若请求为空或者给所有电梯分配完一次请求后,进入wait()。
待读取新的请求或者某个电梯的主副请求队列发生改变时,被notify,然后进行任务分配,notify其它所有的电梯线程。 -
电梯运行
若主副请求均为空,进入wait()。
待调度器分配任务后被notify,然后运行电梯。
-
调度算法
-
调度器请求分配算法
- 流程图
- 部分优化特点
- 采用重分配原则,即通过“乘客挑选最近电梯”、“电梯从选择该电梯的乘客中挑选最近的乘客”两个步骤分配完第一个电梯后,第二个电梯的“乘客选择该电梯”的请求可能会变化(因为可能相较于前一次分配,会把前一次没被电梯选择的请求加入到这个请求队列中)。若不采用这种方式,性能会大大折损,例:
将请求1,2,3分配给A,B电梯->第一次分配,请求1,2,3均选择电梯A->电梯A选择最近的请求1->由于1,2,3均选择了A,B无请求可挑选 - 采用直达优先原则
即纵使一辆电梯离某个请求最近,若其不能满足乘客直达,乘客依然不挑选这辆电梯。 - 分配请求不超过电梯最大人数要求
记录当前电梯状态,为其分配副请求直到电梯内人数加副请求数量等于电梯最大人数限制为止。
- 采用重分配原则,即通过“乘客挑选最近电梯”、“电梯从选择该电梯的乘客中挑选最近的乘客”两个步骤分配完第一个电梯后,第二个电梯的“乘客选择该电梯”的请求可能会变化(因为可能相较于前一次分配,会把前一次没被电梯选择的请求加入到这个请求队列中)。若不采用这种方式,性能会大大折损,例:
-
电梯运行流程
- 流程图
- 部分优化特点:
- 将开关门独立于需要锁的changePeople函数之外(负责上下人的函数块)。使得此电梯休眠期间别的电梯可以进入需要此锁的函数中。。
- 换乘乘客楼层选择:
记乘客当前搭乘电梯为A,其它电梯分别为B1,B2,B3,选择能到达该乘客最终目标楼层的电梯,不妨是B1,B2,分别计算A能到达的楼层与其能到达的楼层的交集,不妨设为Z1,Z2,从Z1,Z2中选择目前离该乘客搭乘电梯当前楼层最近的楼层作为该乘客的换乘楼层,从而确定换乘电梯。(但是不考虑其它电梯的状态的话,这样的换乘方式某些情况还是比较低效)
- 流程图
四、可拓展性
若要拓展的方面不脱离 “用户输入->调度器调度->电梯响应”这样的结构的话,本代码至少不会达到难以修改的地步,因为添加部分代码要求并不会对除了调度算法和电梯运行算法之外的地方产生影响,函数块的耦合度较低(虽然类之间的耦合度很高),而调度函数块要面临着较大部分的改动,因为这部分算法要做到全知全能,才能对电梯进行任务分配,如第二次作业到第三次作业,新增了电梯楼层限制,那么该函数中请求挑选合适电梯的部分就要完全重写。但是由于这几次作业都在上述的结构限制内,因此更改起来还是比较容易的。
倘若突破了上述限制的结构,那扩展性几乎是0。
五、设计结构度量分析
下面放Designite Java的结果(虽然我感觉这部分对我来说非常的没有用处,因为纵使告诉了我哪些地方有问题,我也比较难以在下次的编写中特意避免这些问题,只能起到“责怪我这次代码写的不好”的作用,而真实的启发,即如果实际去避免这些比较少)
方法复杂度:
这里挑选了几个代码行数和圈复杂度较高的方法,然而我看了一下,很大的原因是因为我没将电梯按类型去编写类,导致判断电梯类型的时候要用大量的if-else,但是我感觉这一部分无可厚非,采用这样的方式反而能够更加的直观一些。
依赖关系:
P2 bug分析
-
自我bug分析
虽然强测和互测中均为找的我的bug,但是我还是觉得我程序有些地方的书写很奇怪,感觉许多地方没有产生bug是由一部分的特判强撑着,稳定性很差。
最让我感觉奇怪的地方就是之前为了优化乘客上下部分而将开关门从这部分提取到函数外面,更改乘客的部分就由原本的整体变成了"判断是否开关门->开门->上下乘客->关门",而判断是否要开关门与上下乘客的部分原本应当是衔接的,这两部分都要保持共享数据requests的不变,这样就可能导致出现由于期间共享requests的改变,导致任务重分配,导致本来应当在此楼接的乘客分配到别的电梯中去的情况,即"判断应当开门->开门->(世界线变动)空操作->关门"的情况。
不过还好这几次作业并不把无意义的开关门作为错误的点,而导致我能够苟过,而且由于现阶段的电梯特性好像不会出现上述空操作的情况。但是这依然是一个潜在风险,例如电梯的可换乘楼层随时间变化的话,可能就会在变换后导致空操作的产生。 -
发现别人程序bug所采用的策略
使用评测姬挂一天就完事了。由于本人知识与能力有限,不会写评测机程序,因此白嫖别人的评测姬。
(特此鸣谢:pbnb!)
P3心得体会
这个单元的作业让我理解到了设计架构的重要性,以及向他人学习的重要性。第一单元的作业由于架构的混乱+不吸取他人经验+无脑的上手就开写导致我的作业屡次遭重,而且耗费了大量的时间。但是这次由于参考了某些学长的UML框架图(其实就是架构),以及电梯运行的算法,因此写起来作业感觉轻松多了。
这个单元也让我明白了:
必要与熟练的知识基础+良好的设计架构+提前的较为完整的构思 = 一份轻松且质量较好的代码。