UnitTwoSummary
目录
- 一、设计策略与程序分析
- 第一次作业
- 第二次作业
- 第三次作业
- 二、可扩展性检查与分析
- 三、bug
- 四、总结与反思
一、设计策略与程序分析
第一次作业
设计思路
输入,调度器,电梯分别设置成三个线程
调度器采用单例模式
请求信息封装为不可变类
电梯设置成具有Running
,Opening
,Closing
,Waiting
四种状态的状态机:电梯只需要根据调度器设定的信息在四种状态之间进行切换。
线程通信采用Lock
&Condition
的await
和signal
方法:相比于wait
和notify
的搭配,同一个Lock
设置不同的Condition
,方便对特定线程的唤醒。
调度算法采用look
:当运行方向上的楼层不再有等待完成的请求且电梯内的请求已经处理完毕时,调转电梯运行方向。
时序图:
UML:
LinesCounter:
Metrics:
调度器和电梯类的复杂度较高,未能及时将功能分离
第二次作业
设计思路:
1.大致思路
两种线程:主线程和电梯线程
主线程处理输入并将需求放置到相应的楼层类
电梯作为一个状态机按照主线程设置的信息运转
2.优化&调度策略
①优化:楼层类
把每一楼层当成对象,共19个对象,每个楼层对象包含一个请求队列。
把主线程当成生产者,电梯当成消费者,楼层类当成托盘(单生产者多消费者模型)
有新需求时,主线程按照新需求的from
楼层将其放入对应的楼层对象;电梯需要获取请求时访问与电梯当前楼层对应的楼层对象。
楼层类的好处:相比于把所有新请求放入同一个队列,楼层类可以保证不同楼层的请求队列能被不同电梯或生产者同时访问,增加效率,节约时间
19个楼层类和5个电梯实现的道理相同,都是在初始化时重复new
一个相同的类(楼层类或电梯类),然后建立楼层(或类型)和对象的映射关系,之后的访问也是根据映射关系访问即可。
②调度策略:随机分配
当有新需求到来的时候,选择运行方向和新需求方向相同的电梯,通知此电梯有新的同方向的需求,此处只是通知而不是将需求和电梯绑定
需求最后由哪个电梯取走完全取决于几个同方向运行的电梯中哪个先到达需求的from
楼层(类比现实生活,应该也是哪个电梯先到进入哪个电梯中)
因为不知道需求最后被哪个电梯取走,所以是随机分配
随机分配的关键是不将需求和电梯绑定,直到需求进入电梯的子队列中,才确定需求由哪个电梯实现
随机分配的前提是几个电梯都是一样的,无论是电梯可以到达的楼层,还是楼层间运行的速度
随机分配的好处是当一个电梯到达一个楼层时,能装多少就装多少,提升效率
当然电梯可能出现陪跑的现象,但不增加总时长(因为当他发现同方向已经没有需求时(被别的电梯装走了),他就会掉头或等待)
3.设计中的思考
物尽其能:
比如将人的行为还给人。上下电梯时的输出,封装在人的类里,人需要上下电梯时,直接调用方法即可
传递的是对象:
比如调度器需要获取电梯的若干信息,可以将这些信息封装成类,这样调度器获取时,只需要先生成一个状态对象,然后传递给调度器。(return new State(...)
)即可
时序图:
UML:
LinesCounter:
Metrics:
虽然将电梯已经拆分lift和liftstate,但复杂度仍然较高,下一次作业进行了进一步拆分
第三次作业
设计思路:
1.大致思路
三种线程:主线程、调度器线程和电梯线程
主线程处理输入并将需求放置到buffer中
调度器线程不断从buffer中获取请求,并调用Separator对请求拆分,然后将拆分后的请求放到对应的楼层中
电梯线程不断从楼层类中获得请求,并处理请求
2.优化
①楼层类plus:
把每一楼层当成对象,共23个对象。
每个楼层对象包含三个请求队列:up
,down
,wait
up
:存储当前楼层的向上就绪请求
down
:存储当前楼层的向下就绪请求
wait
:存储需要从当前楼层换乘但需要等待前序就绪请求完成的后序等待请求
按照方向以及请求是否就绪进一步细化当前楼层的请求
(读者如果不知道什么是楼层类,请看上周推送)
②hang_out
hang_out:比如一个8到-3的请求,需要BA两类电梯的配合,假设当B尚未将拆分的前序请求送到-2层时,A此时已经空闲,那么就让A去-2(某个存在后序等待请求的换乘楼层)提前等着,如果在A去-2的途中来了任务,那就优先执行任务。
3.拆分&调度
①拆分:
核心思想:能直达即直达、无法直达再拆分
直达:一个请求的from
和to
都是某一类电梯的可停靠楼层,则直接将此请求分配给此类电梯
直达分配顺序:C
,A
,B
即一个请求C,A
都能完成,优先分配给C。
直达分配顺序的确立依据:优先级与可停靠楼层数量成反比(即笔者认为C可能会比较闲,所以一个能让C完成的请求,直接分配给C)
如果不能直达,再考虑拆分:
确定换乘楼层:-2,1,5,15(根据能直达即直达的思想确定,即一个从8到-3层的请求,B最远能到-2,此时再换乘,由A送到-3,其他换乘楼层确定同理)
根据换乘楼层拆分不可直达的请求,将拆分后的请求按照链式存储,即在Person
请求类中存储nextPersonRequest
②调度:
不同类的电梯执行按类分配的请求,彼此无竞争;同类的电梯相互竞争同类的请求
即对于一个电梯:不是你要完成的请求(按类划分,而不是按id
分配),你别管;是你要完成的请求,如果电梯还没满,就把人装上。
时序图:
UML:
LinesCounter:
Metrics:
已将电梯分成lift,liftController,liftState三类,有效降低了复杂度
拆分请求的Separator判断逻辑较多,所以复杂度较高
二、可拓展性检查与分析
1.按照SOLID原则检查
- 单一功能原则:第三次作业将电梯分成Lift,LiftController,LiftState三类,各司其职,拆分设置的拆分器Separator,各类功能较单一
- 开闭原则:如果有新的拆分策略,可以继承拆分器,无需对拆分器本身修改,电梯如果新的状态需要记录,同样继承LiftState方法,增加属性、set、get方法等,无需对lift,liftState,liftController修改,满足了开闭原则
- 里氏替换原则:程序中未涉及继承,所以未涉及里氏替换原则
- 接口隔离原则:程序中未涉及接口,所以未涉及接口隔离原则
- 依赖反转原则:三类电梯只依赖同样的lift类,楼层对象也是复用的相同的楼层类,可以看出能较好的做到抽象,满足依赖反转原则
2.可拓展性分析
设计过程中调度器是对从buffer中取出的请求,先拆分然后一次分配到位,这种设计有拓展的局限性,如果未来增加动态退出电梯的需求,那么这种一次到位的分配反而增加复杂度。而如果改成每次电梯处理完成一个请求,然后判断是否为换乘请求,如果是换乘请求,那么再将请求交由调度器进行分配,这样在未来比如增加动态退出的电梯的需求时,可以减少分配的次数,简化逻辑。
三、bug
强测与互测的bug为RTLE(real time limit error)
原因在于程序的线程安全问题:电梯最后停不下来
比如如下bug:
@Override
public void run() {
...
while (!end) {
waitForRequest();
execute();
}
...
}
public synchronized void setEnd() {
end = true;
notifyAll();
}
public synchronized void WaitForRequest() {
...
//if (end) {
// return;
//}
...
wait();
...
}
如果不添加waitForRequest
中注释的判断,那么很有可能在run
中while
判断后此时其他线程调用setEnd
方法然后再进入waitForRequest
方法中,那么电梯可能就会永久的等待下去
四、总结与反思
虽然自己在完成多线程单元的三次任务之前已经开发过涉及多线程的软件,但仍出现线程安全bug,主要原因在于盲目自信,因为本地的评测环境不如课程组设置的评测环境,所以在本地测试中并没有发现这种线程安全bug,在评测过后继续对反馈的出错数据进行测试,仍然无法复现,这对发现bug和debug十分不利,只能反复对程序论证,验证涉及线程通信的各种情况。
在未来的多线程程序的开发中,也可能出现这种无法复现性,不应盲目的依赖测试数据,需要对可能产生线程安全的地方进行反复论证,确保极端情况下,程序也不会crash。
在经历了0bug的第一单元和若干bug的第二单元之后,已经能正确理解和看待OO课程的各种制度,感谢在第二单元debug的过程中提供帮助的舍友fty,希望能在未来的单元中戒骄戒躁、稳步前行。
Fighting For Better Future of OO Together!