北航OO第二单元总结-电梯调度

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

北航OO第⼆单元总结-电梯调度
综述
总体采⽤⽣产者-消费者模式,三次作业总体架构没有⼤的变化,只在每次新增任务时修改相关的细节。

每⼀个请求处理流程:输⼊线程->调度器->电梯
电梯,实现基本移动和电梯内状态查询,有配套的电梯队列
策略,控制电梯移动
调度器,从总请求池中分配请求⾄电梯队列
同步块设置和锁的选择
第⼀次作业相对⽐较简单。

但由于对于多线程的不熟悉,关于如何设置同步也思考了很久。

分析线程访问,主要是针对于存储request的容器需要同步控制,输⼊线程写⼊、调度器取出会进⾏写操作,经过分析之后,对于如何控制同步块就有了眉头。

⽣产者-消费者模式,输⼊线程往请求池中放⼊请求,消费者需要判断在请求池为空时等待,由于涉及到访问状态,需要synchronized保护访问。

synchronized (waitTable) {
if (waitTable.noWaiting()) {
try {
waitTable.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
以及为了保证共享数据的正确性,有容器(请求池)中的synchronized⽅法,例如
public synchronized Request getRequest();
public synchronized void delRequest();
但是这样做还有⼀些问题,就是消费者如何知道⽣产者已经停⽌了呢?这⾥⼀开始⼤致想了两种⽅法,⼀是⽣产者离开时放⼊⼀个特殊请求-1 to -1(poison)当消费者获得该请求时停⽌线程,⼆是增添请求池状态。

后来上机时的架构感觉更加易于操作(但似乎有bug?),最后也采⽤了这种⽅法,⽣产者得到停⽌信号后关闭候乘表类,消费通过候乘表获得相关信息。

synchronized (waitTable) {
if (request == null) {
waitTable.close();
waitTable.notifyAll();
}
}
候乘表关闭状态,消费者可查询得到⽣产者已经结束⽣产的消息。

第⼆次、第三次作业的同步控制部分。

由于每个电梯的访问是被保护的,所以多个电梯也⼤体类似,主要的地⽅是考虑到新加⼊电梯可以进⾏优化、换乘时调度器对于请求的⽀配能⼒,调度器需要实现把电梯中⼀部分请求迁移⾄新加⼊的,或者更加合适的电梯中,需要设置同步。

synchronized (elevatorQueues) {
// redraw() or dispatch();
}
调度器设计
三次作业的调度器是⼀个迭代开发的过程。

第⼀次作业。

由于只有⼀部电梯,主要是为了后续扩展性,没有太多的控制。

第⼆次作业。

⾸先是如何分配请求的问题,主要考虑两个⽅⾯,⼀是电梯所在楼层,⼆是电梯内部⼈数。

这两者有⼀定的权重,举个例⼦,如果电梯离某个请求很近,并且电梯内⼈数很少,调度器会把该请求分给这部电梯。

但是如果电梯内⼈数很多,经过加权可能不如另⼀个距离较远的空闲电梯,则调度器会分给空闲电梯。

新增电梯也可在此基础上进⾏调度,抽取⼯作权重较⼤电梯的请求,放⼊⼯作权重较⼩的请求,让每部电梯负载均匀。

调度器需要对请求池、电梯队列进⾏交互和相关状态的查询,最终进⾏分配。

具体来说是
输⼊线程得到输⼊后放⼊请求池,唤醒调度器
调度器从请求池中取出请求,调⽤每⼀部电梯的状态查询⽅法,根据每个电梯不同状态以及请求的起点和终点,经过⼀定的加权选择之后将该请求分配给⼀个最为合适的电梯队列中。

(或者是电梯请求,也进⾏相关的查询和分配)
⾄此,调度器的⼯作结束,问题就变成了单个电梯的运⾏。

第三次作业。

有换乘的需求,所以调度器需要决策请求是否需要换乘,这⽅⾯我只进⾏了较为简单的优化。

⾸先⾃然是根据需求分配最匹配的电梯,例如1-20这种优先考虑分配给C,在此基础上
奇偶换乘,并且距离较远(⼤于设定阈值,作业中采⽤8层),A-B换乘
长途运输,例如from 5 to 20,会根据当前情况先到1-3层中转,之后由C电梯处理
总体上来看对于⼤部分数据的性能较好,但是对于⼀些极端的数据,虽然有动态的加权计算,但是整体上由于策略较为固定,容易产⽣“虽然进⾏了良好的分配,但总时间却增加”的情况。

架构设计
第⼀次作业
UML类图
sequence diagram
第⼆次作业
UML类图
sequence diagram
第三次作业
UML类图
可扩展性
架构设计上分为输⼊、电梯、策略、调度器四部分。

电梯部分只进⾏简易移动,对于三种模式配置三种策略类适应调度。

对于不同种类的电梯区分,主要在于调度器的判断,电梯只是⼀个电梯,并不关⼼其他东西。

如果需要实现功能扩展,⼀般不需要改动电梯,需要改变电梯的相关配置类、调度器。

对于不同的模式,配置不同的Strategy类
对于不同电梯的调度、换乘请求,修改调度器以分配⾄不同的电梯队列。

虽然这样带来了⽐较好的扩展性,但还是有⼀定不⾜,例如调度器类承担了太多⼯作,存在的耦合较⾼等等,在后来课堂上讲解设计原则时,对⽐发现⾃⼰的设计还能做得更好。

对于策略模式,相⽐于使⽤⼤量的if else,使⽤策略模式可以降低复杂度,使得代码更容易维护。

但缺点是需要有很多的策略类,使得整体复杂度变⾼。

性能设计⽅⾯,性能⽅⾯还有很多可优化的地⽅。

对于⼀部电梯⽽⾔,由策略驱动电梯运转,策略只关⼼电梯队列中的请求如何选择。

Random模式
并不是标准look电梯,⽽是根据电梯位置、电梯状态对⼯作负载进⾏加权计算,选择⼀个⼯作权值较⼩的电梯,在⼀定范围内可以反向接⼈之后继续前进。

Morning模式
根据可⽀配电梯的⽬前状态,集齐较多⼈数之后再启动。

Night模式
先上到请求的最⾼层,⼀趟带⼊底层。

对于多部电梯,需要调度器考虑请求池中请求分配问题,以及是否需要对该请求采取换乘策略。

如上⽂所述,核⼼可以概括为两种,奇偶换乘和⾼低换乘。

由于采取了较为细致的判别⽅式,所以调度器需要查询电梯状态、电梯队列的情况,对每个电梯遍历之后才能做出决策。

在可扩展性⽅⾯,这并不是⼀个好的选择,基本上耦合了除输⼊线程以外的其他核⼼类,使得调度器不利于维护。

这样考虑优化实际上有些死板,策略较为固定,⾯对动态的数据难免出现不⾜,但在基本都有不错的性能分,不会出现太差的情况。

研讨课对于算法的讨论很多,动态计算、打分等等⽅法或许在⾯对不同数据有更好的适应性。

分析⾃⼰程序的bug
第⼀次作业
第⼀次作业较为简单,公测和互测中未发现bug
第⼆次作业
第⼆次作业出现bug主要在某种执⾏顺序下,新电梯直接进⼊等待。

原因主要在于调度器如果和⼀个电梯之间没有任何交互,在重新分配⼈员时如果该电梯还是没有分到请求,将会睡眠,最后调度器结束时没有唤醒该电梯。

核⼼代码段在电梯类的run()⽅法中,判断线程停⽌条件
private boolean needWait() {
return processingQueue.isEmpty() && elevatorQueue.isEmpty() && !elevatorQueue.isEnd();
}
可以看到,run()⽅法相对复杂,在判断停⽌的逻辑也⽐较复杂,导致疏忽了判断。

⾸先分析问题出现的场景,新建造的电梯产⽣时,各电梯的权重⾮常均匀,导致新没有和调度器进⾏交互,直接进⼊等候状态,调度器后续也不对该电梯进⾏任何分配,直⾄结束。

这样的结果是调度器结束了,但电梯还在等待。

如何解决这个bug呢?经过waitTable与inputThread之间的交互的经验。

同样采⽤了⼀个电梯队列的状态,当调度器结束时通知每⼀个电梯队列,这时即使新电梯没有经过任何调度,⼀旦收到了停⽌信号,醒来后就会检查电梯队列,在处理完毕后结束线程。

第三次作业
第三次作业的bug在于night模式的电梯分配问题,位于分配类的handlePriority()⽅法中,这是⼀个后来加⼊的针对优先匹配的请求的权重的分配⽅法。

bug产⽣的原因在于night模式下进⾏了额外的优化选择,结果在优化时的疏忽,把电梯型号写错,导致触发优化条件的乘客,调度器会将这种优化的乘客放⼊错误的电梯型号,导致到达楼层产⽣错误。

互测未发现其他bug
发现bug的策略
如果通过静态分析,很难在不理解别⼈架构的情况下发现bug,所以采⽤测试数据测试。

第⼀次作业中发现同组有1-20或20-1连续出现时,最后⼀个乘客未被响应的bug,⼤概5次能复现⼀次。

提交了⼀次,但没有复现,于是作罢。

后续作业均只进⾏了⼀些测试数据检测,没有发现明显bug,由于其他课程的压⼒没有进⾏太多静态分析。

线程相关的问题,可以采⽤JProfiler插件来检测每个⽅法和每个线程的执⾏和运⾏状态,框定⼀个⼤致范围,可能还是需要进⾏静态分析来定位bug。

与第⼀单元相⽐,测试的策略基本相似,主要通过测试数据进⾏⿊盒实验,但相⽐之下,线程安全的bug可能并不容易复现,可能需要特定的状态和执⾏顺序才能触发。

如果想要找到这类问题,需要仔细分析代码,所以在第⼆单元中对于代码的静态分析相对来说更为重要。

⼼得体会
永远要⼩⼼线程安全(没有发现bug不代表没有bug),多线程的并发带来了调试的困难,如果没有合理的同步控制,可能在某种情况下就出现了并发访问共享变量的的错误,或者不同线程先后拿到锁,但都在等待另⼀把锁的经典死锁问题。

编写多线程代码时可以做好阶段性的log记录,如果遇到了难以复现的bug,有时难以调试,这时可以及时查看前⼀阶段的记录情况,合理分析从⽽减轻debug的⼯作量。

层次化设计。

好的架构可以减少bug的产⽣,还能更好地进⾏迭代开发,进⾏相关优化。

经过第⼀单元的学习,在第⼆单元的⼀开始的架构就构思了⽐较久,给后续的迭代开发留下了⾜够的空间,尽量将每个部分的功能分隔开,电梯只进⾏简单的运动,输⼊也只管输⼊,调度器负责调度,实现了每次作业的额外功能开发。

但总体看来还是有⼀些不⾜之处,例如调度器的设计还是⽐较复杂,不利于维护。

经过两个单元的学习,能明显感觉到设计能⼒的上升,但依然长路漫漫,有很多东西需要进⼀步地学习。

相关文档
最新文档