Unit 2 Summary
by Mike Liu
- Unit 2 Summary
- 代码分析
- Homework 5
- Preview
- 静态分析
- 设计策略
- Homework 6
- Preview
- 静态分析
- 设计策略
- Homework 7
- Preview
- 静态分析
- 设计策略
- 功能设计与性能设计
- Homework 5
- 时序图
- Bug分析
- Homework 5
- Homework 6
- Homework 7
- 心得体会
- 代码分析
代码分析
总的来看,本单元三次作业的架构没有大的变化。
主要的类(线程)有三个:主类(读入线程)MainClass
,电梯Elevator
和电梯控制器Controller
。
- 每个电梯线程均和一个控制器线程形成一一对应关系。
- 主类负责读入请求,并随即决定将请求分配给某一部电梯,加入该电梯的控制器的请求队列中。当输入结束时,读入线程即结束。
- 控制器负责根据电梯的状态来将请求送入/取出电梯。当电梯线程结束时,控制器才结束。
- 电梯自动根据电梯内部的请求确定目标楼层并运行;当内部没有请求时,自动从控制器处获得一个目标楼层;每到达一层时,会自动根据电梯内部请求和控制器中已有的请求决定是否开门。只有当控制器中没有请求且输入已经结束时,电梯线程才结束。
控制器中的请求队列使用ConcurrentLinkedQueue
储存,每次电梯开门时即遍历该队列,将需要进电梯的请求从中移除并送入电梯。
Homework 5
Preview
本次作业较为简单,调度策略的核心在于电梯如何确定主请求。
静态分析
-
方法分析
Method CONTROL ev(G) LOC V v(G) Controller.Controller(Elevator) 0 1 4 13 1 Controller.addRequest(PersonRequest) 0 1 4 19 1 Controller.endInput() 0 1 4 11 1 Controller.endRequest() 0 1 3 8 1 Controller.feedPerson() 2 1 14 167 4 Controller.getMainRequest(int) 6 4 29 528 9 Controller.requestEnded() 0 1 3 24 2 Controller.run() 6 1 24 166 7 Controller.someoneToGetIn(int) 2 3 10 120 4 Elevator.Elevator() 0 1 4 16 1 Elevator.doorClose() 3 2 14 89 3 Elevator.doorOpen() 3 2 15 99 3 Elevator.endService() 0 1 3 8 1 Elevator.getController() 0 1 3 8 1 Elevator.getFloor() 0 1 3 8 1 Elevator.getHead() 0 1 3 8 1 Elevator.getIn(PersonRequest) 0 1 5 46 1 Elevator.getMainRequest() 6 1 24 292 9 Elevator.getMainRequestFromController(boolean) 7 1 25 322 13 Elevator.getOffAll() 3 1 16 202 5 Elevator.goOneFloor() 3 2 16 133 5 Elevator.isOpen() 0 1 3 15 1 Elevator.run() 2 1 11 79 5 Elevator.serviceEnded() 0 1 3 8 1 Elevator.someoneToGetOff() 2 3 8 51 3 MainClass.main(String[]) 3 3 15 99 3 -
类分析
Class CBO DIT LCOM LOC NOAC OCavg Controller 3 2 1 101 7 2.33 Elevator 2 2 1 169 14 2.31 MainClass 2 1 1 17 1 3 -
类图
设计策略
可以看出,复杂度主要体现在电梯的getMainRequest
函数;该函数是第一次作业调度策略的核心。
- 当电梯内部有请求时,该函数取出电梯内请求队列中到达楼层距当前楼层最远的一个作为主请求;
- 当电梯内部无请求时,该函数通过
getMainRequestFromController
流程,调用控制器的Controller.getMainRequest
函数,从控制器获得一个目标楼层。当控制器中无请求时,电梯线程进行wait
,直到控制器中有新的请求,或所有输入结束。
Homework 6
Preview
本次作业加入了多部电梯,核心在于请求的分配。
静态分析
-
方法分析
Method CONTROL ev(G) LOC V v(G) Controller.Controller(Elevator) 0 1 4 13 1 Controller.addRequest(PersonRequest) 0 1 4 19 1 Controller.calcWeight(PersonRequest) 3 1 19 436 6 Controller.endInput() 0 1 4 11 1 Controller.endRequest() 0 1 3 8 1 Controller.feedPerson() 4 4 17 185 5 Controller.getMainRequest(int) 6 4 29 528 9 Controller.requestEnded() 0 1 3 24 2 Controller.run() 6 1 24 166 7 Controller.someoneToGetIn(int) 2 3 10 120 4 Elevator.Elevator(String) 0 1 5 28 1 Elevator.doorClose() 3 2 15 105 3 Elevator.doorOpen() 3 2 15 105 3 Elevator.endService() 0 1 3 8 1 Elevator.getController() 0 1 3 8 1 Elevator.getFloor() 0 1 3 8 1 Elevator.getHead() 0 1 3 8 1 Elevator.getIn(PersonRequest) 1 2 8 99 2 Elevator.getMainRequest() 6 1 24 292 9 Elevator.getMainRequestFromController(boolean) 7 1 25 322 13 Elevator.getNumOfPerson() 0 1 3 11 1 Elevator.getOffAll() 3 1 18 247 5 Elevator.goOneFloor() 3 2 16 208 7 Elevator.isFull() 0 1 3 19 1 Elevator.isOpen() 0 1 3 15 1 Elevator.run() 4 3 13 109 7 Elevator.serviceEnded() 0 1 3 8 1 Elevator.someoneToGetOff() 2 3 8 51 3 MainClass.main(String[]) 10 5 40 668 9 RequestComparator
类中只包含了一些用来比较两个请求的静态方法,此处不列。 -
类分析
Class CBO DIT LCOM LOC NOAC OCavg Controller 4 2 1 123 8 2.7 Elevator 2 2 1 187 16 2.39 MainClass 2 1 1 42 1 8 RequestComparator 1 1 3 44 9 1.67 -
类图
设计策略
可以看出,本次作业相较上一次,主类(读入线程)的复杂度有了很大提升,因为其负责了请求的分配。
由于是多部电梯,且每部电梯有载客量限制,因此分配主要考虑的因素主要有两点:一是当前电梯及其控制器中已有的请求数,二是新请求捎带其他请求/被其他请求捎带的能力。
综上所述,本次作业采用了加权随机的分配策略,具体策略如下:
- 每读入一个新的请求,根据该请求为每个电梯计算一个权重\(weight\):该权重最大值为1000,由两部分组成:其中已有请求数\(numWeight\)贡献70%,可稍带/被捎带的能力\(coverageWeight\)贡献30%。
- 已有请求数贡献的权重\(numWeight = 700\times2^{-totnum/9}\) ,其中 \(totnum\) 为电梯及其控制器中当前已有的请求数之和。
- 可稍带/被捎带的能力贡献的权重
\(coverageWeight = 300\times\begin{cases}1, \text{当前请求起止于电梯当前层到目标层之间,且同向}\\0.75, \text{当前请求起于电梯当前层到目标层之间,且同向}\\0.5, \text{当前请求起止于电梯目标层之后,且同向}\\0.5\times2^{-diff/3},\text{其他 其中diff为当前目标层与请求起始层的楼层差绝对值}\end{cases}\);
RequestComparator
类中的静态方法即是来完成对不同情况的判断。
得到每个电梯的权重后,用随机数决定将请求分配给哪部电梯;被分配到的概率和权重成正比例关系。决定后,随即将该请求加入对应电梯的控制器的请求队列。
之所以采用随机策略,主要出于两点考虑:一是权重计算本身并不精确,没有用数据进行测试,只是凭感觉;二是调度时间本身受到很多不确定性因素影响,即使权重最高也不一定最优,因此用随机的方式避免一些最坏情况发生。非常遗憾,由于本次作业出现了其他致命错误,导致强测全部WA,没有能够验证该策略的效果。
Homework 7
Preview
第三次作业的核心是停层限制和换乘;因此核心是完成对请求的拆分,其余机制和功能不变。为了便于拆分请求,引入了自定义的请求类MyRequest
,封装了PersonRequest
原有的接口,增添了对前驱/后继请求的引用,以及对请求的激活功能。
静态分析
-
方法分析
Method CONTROL ev(G) LOC V v(G) Controller.Controller(Elevator) 0 1 4 13 1 Controller.addRequest(MyRequest) 0 1 4 19 1 Controller.calcWeight(MyRequest) 3 1 19 436 6 Controller.endInput() 0 1 4 11 1 Controller.endRequest() 0 1 3 8 1 Controller.feedPerson() 7 5 20 254 7 Controller.getMainRequest(int) 8 5 30 567 10 Controller.requestEnded() 0 1 3 24 2 Controller.run() 6 1 24 166 7 Controller.someoneToGetIn(int) 2 3 9 124 5 Elevator.Elevator(String,String) 9 2 28 247 5 Elevator.doorClose() 3 2 15 105 3 Elevator.doorOpen() 3 2 15 105 3 Elevator.endService() 0 1 3 8 1 Elevator.getController() 0 1 3 8 1 Elevator.getFloor() 0 1 3 8 1 Elevator.getHead() 0 1 3 8 1 Elevator.getIn(MyRequest) 1 2 8 99 2 Elevator.getMainRequest() 7 1 25 332 10 Elevator.getMainRequestFromController(boolean) 7 1 25 322 13 Elevator.getNumOfPerson() 0 1 3 11 1 Elevator.getOffAll() 3 1 20 269 5 Elevator.getType() 0 1 1 11 1 Elevator.goOneFloor() 3 2 16 208 7 Elevator.isFull() 0 1 3 19 1 Elevator.isOpen() 0 1 3 15 1 Elevator.run() 4 3 13 137 8 Elevator.serviceEnded() 0 1 3 8 1 Elevator.someoneToGetOff() 3 4 9 102 5 MainClass.main(String[]) 14 8 54 894 13 MyRequest.Link(MyRequest,MyRequest) 0 1 4 25 1 MyRequest.MyRequest(PersonRequest,String) 0 1 4 11 1 MyRequest.activate() 1 1 4 30 1 MyRequest.activateNext() 1 2 4 26 2 MyRequest.getFromFloor() 0 1 3 15 1 MyRequest.getPersonId() 0 1 3 15 1 MyRequest.getToFloor() 0 1 3 15 1 MyRequest.getVia() 0 1 1 4 1 MyRequest.getViaType() 0 1 1 11 1 MyRequest.isActive() 0 1 3 15 1 MyRequest.setVia(Elevator) 0 1 1 8 1 -
类分析
Class CBO DIT LCOM LOC NOAC OCavg Controller 5 2 1 126 8 3 Elevator 3 2 2 227 17 2.63 MainClass 4 1 1 56 1 12 MyRequest 5 1 3 38 10 1.09 RequestComparator 2 1 3 44 9 1.67 RequestSplitter 3 1 1 262 1 4 RequestSplitter.Node 1 1 0 13 0 1 -
类图
总的来说,本单元各线程分工比较明确,在类的设计上没有特别的注意点。
设计策略
-
换乘拆分
-
用图的思想,将每一层和每类型电梯在每一层的停靠点作为图的结点,进出电梯、电梯上下运行的过程作为图的边。
-
为了最大限度地减少换乘,将表示电梯上下运行的边长度设为实际运行需要的时间,将从楼层进出电梯的边长度设为一个很大的值。
-
将每一层视为起点,分别跑一次SPFA,求出到其他各层的最短路径。若无需换乘,则只求出需乘坐的电梯型号;若需要换乘(本题目中最多换乘一次),则求出两次需乘坐的电梯型号和换乘楼层。
该部分其实可以内嵌在Java程序中进行。但由于用Java描述算法过于臃肿,且为了减少不必要的CPU开销,本次作业书写了额外的C++代码用于执行SPFA算法最短路并打表,并将打表结果转化为Java语句,写入了
RequestSplitter
类的static
域。 -
-
请求设计
MyRequest
中封装了PersonRequest
,并且存储了前驱请求prev
和后继请求next
的引用。当一个初始请求被拆分成两个新请求时,调用MyRequest.Link
方法分别设置两个请求的next
和prev
为对方。- 当一个请求的
prev
不为null
时,表示该请求的前驱请求尚未完成,现在不能调度。 - 当一个请求的
prev
为null
时,表示该请求的前驱请求已完成或没有前驱请求,现在可以调度。 - 当一个请求的
next
不为null
时,表示该请求有后继请求;该请求结束(出电梯)后,将后继请求的prev
置为null
,使得后继请求可以被调度。
这样,只需要在调度器中添加判断条件(当前请求是否有前驱请求)即可,其余皆可继承之前的作业。
- 当一个请求的
功能设计与性能设计
从功能角度来看,由于第一次作业的架构就具有很好的一般性,导致电梯系统的兼容性很强:只需要做好请求的分配即可,无需调整整体的架构。这样的好处在后两次迭代中也显现出了优势,每次迭代修改的代码量很少。这样即使后期有更多功能需求,也便于扩展;只需要修改上层的逻辑。
从性能角度来看,由于必须在请求读入时就确定分配方案,调度机制不能机动地考虑到实时的情况,只能根据预定的方案和目前电梯的状况来进行分配,显然缺乏一些灵活性,且不是性能最优解。当然,可以通过后期修改参数来调整分配策略,使性能不断优化;也可以设置不同的参数,来适应不同工作情况。
总体来说,本单元作业更倾向于功能设计,牺牲了部分性能。但还可以增加一些附加机制,如将已进入控制器的请求重新分配等,来提高性能。以及,由于写第一次作业时对多线程还不够熟悉,一些同步关系比较混乱,基础架构还存在提升的空间;尤其是电梯线程和控制器线程的配合,还可以有更好的解决方法。
时序图
Bug分析
Homework 5
强测和互测中均未出现bug。
Homework 6
本次作业中出现了一个bug,导致强测所有测试点WA。
在主类(读入线程)中,同时使用了Scanner(System.in)
读入电梯个数和ElevatorInput(System.in)
读入电梯请求,产生了缓冲区冲突。
在中测时,由于同时进行评测的进程较少,读入线程被及时调度:Scanner
读取第一个整数时,后续请求还未被输入,此时Scanner
只取走这个整数到缓冲区;之后的请求被ElevatorInput
取走,运行正常。
在强测时,由于同时进行评测的进程较多,观察到第一条输出的时间戳往往为数秒之后,说明读入线程未被及时调度:Scanner
读取第一个整数时,后续请求已经被输入,此时Scanner
为了提高读入效率,将请求也取入了缓冲区;当ElevatorInput
读入时,就取不到Scanner
缓冲区中的前几个请求,导致了程序错误。
事后我也和助教讨论了这一bug,提出了我的一些关于评测的疑问,不过我们的助教似乎不太擅长回答问题。其实我们曾经就遇到过类似的情况,比如cin
比scanf
慢,是因为cin
要保持某种和scanf
的同步,来使得两种输入可以同时使用而不出现错误;可以理解为二者用这种同步来保证不会将输入被不正当地取入缓冲区。如果用std::ios::sync_with_stdio(false)
来关闭同步,cin
就和scanf
速度相当,但不能混合使用。
Homework 7
强测全部通过;由于上一次作业没有进入互测,导致了一些可能是遗留的bug,在本次互测中体现出来。
- 电梯开门运行逻辑不严密,在某些情况下到达目标层后不开门,导致卡住不动。
- 调度器每执行一次循环中的
feedPerson
动作就必然唤醒一次电梯,导致电梯在暂时没有请求时不断wait - 被唤醒 - wait - 被唤醒……;更改后,只有在的确有人被feed入电梯的情况下才唤醒电梯,否则不唤醒。
由于采用了随机策略,在某些情况下复现bug有一定的难度。调试时,主要采用了分时段手动输入+print输出的方法,主要查找线程等待与唤醒相关的部分,十分有效。
心得体会
第一次接触到多线程编程,有很多概念需要重新理解。编程思路也和单线程编程有很大的区别:提前的架构设计非常重要,因为后期迭代开发时重构带来的成本过高,设计时必须想清楚协作逻辑,留下良好的兼容性。
除了多线程编程思想之外,也是用一次强测爆0的成本意识到了一些问题,比如输入输出涉及的一些原理。
总之,多线程编程的思维量大幅度提升,需要考虑的问题也增多;要掌握这项技术还需要多加练习。